Кликер с миссиями и настройками
Описание игры
Это динамичная игра-кликер, где задача игрока – как можно быстрее и точнее стрелять по движущимся целям на экране. Цели бывают двух типов: обычные зелёные и быстрые красные. При попадании очки увеличиваются, а при серии подряд – комбо накапливает бонусы. Игра разбита на уровни, которые повышают сложность (ускоряют и увеличивают число целей), а также содержит миссии на убийство определённого количества быстрых целей. Есть выбор длительности игры и уровня сложности. В игре есть звуковые эффекты выстрелов и взрывов, а интерфейс адаптивен под разные размеры экранов.
Возможности игры
- Выбор длительности игры: 15, 30 или 60 секунд.
- Выбор уровня сложности: Лёгкий, Средний, Сложный с различным количеством и скоростью целей.
- Два типа целей: обычные (зелёные) и быстрые (красные).
- Движение целей: плавное перемещение в пределах игрового поля с отражением от краёв.
- Стрельба по целям: создаётся анимация снаряда, при попадании – эффект взрыва, прибавление очков.
- Комбо: каждый выстрел подряд увеличивает комбо, дающее бонусные очки каждые 3 попадания.
- Миссии: каждый раунд игроку даётся задание – убить определённое количество быстрых целей для бонуса.
- Уровни: каждые 30 секунд уровень повышается, увеличивая количество и скорость целей.
- Звуки: тембр выстрелов и попаданий варьируется в зависимости от комбо.
- Интерфейс: отображение очков, таймера, комбо, уровня, сложности и текущей миссии.
- Кнопки управления: Начать игру, Выстрелить, Перезапустить.
- Адаптивность: игра корректно отображается на экранах разной ширины с сохранением пропорций.
- Получение элементов интерфейса по id: игровой контейнер, кнопки, индикаторы очков, уровня, таймера, миссии и результата.
- Объявление переменных состояния: счёт, таймер, комбо, игры, цели, уровень, миссия и параметры сложности.
- Объект
difficultySettingsхранит стартовое число целей и множитель скорости для каждого уровня сложности. - Функция
createTarget(type)создаёт DOM-элемент цели с классомnormalилиfast, задаёт размер и начальные параметры движения. createTargets()очищает и создаёт массив целей исходя из текущего уровня и сложности: увеличивает число быстрых и обычных целей.showTarget(target)случайным образом размещает цель в пределах игрового поля, задаёт скоростиspeedXиspeedYс учётом уровня и сложности, активирует цель и запускает таймер исчезновения и появления заново.hideTarget(target)скрывает цель, делая её неактивной.moveTargets()вызывается в игровом цикле: обновляет позиции активных целей, отражая их от границ игрового поля.- Кнопка «Выстрелить» вызывает функцию
shoot(), которая создаёт визуальный объект снаряда под кнопкой, запускает анимацию вверх. - Во время анимации снаряд проверяется на столкновение с каждой активной целью через
isCollide. - При попадании: увеличивается счёт, комбо, максимальное комбо, начисляются бонусные очки за серии.
- Если цель – быстрая, увеличивается счётчик заданий миссии.
- Воспроизводятся звуковые эффекты с частотой, зависящей от комбо.
- Показывается анимация взрыва (
createExplosion). - Цель скрывается и запускается таймер перезапуска её появления.
- Функция
generateMission()создаёт миссию – убить определённое число быстрых целей, зависящее от текущего уровня. checkMission()отслеживает выполнение миссии, при достижении цели меняет текст подсказки и прибавляет бонусные очки.- Игровой таймер
startTimer()обновляет время каждую секунду, при достижении нуля вызываетendGame(). - Уровень повышается каждые 30 секунд (через
setInterval), меняется количество целей, создаются новые цели и обновляется миссия. - При повышении уровня увеличивается сложность управления и задачи.
- Кнопка «Начать игру» сбрасывает параметры, выставляет базовые состояния, создаёт цели, запускает таймер и игровой цикл.
- Кнопка «Перезапустить» запускает игру заново с текущими настройками.
- Конец игры выключает управление, останавливает движение целей, выводит итоговый результат с учётом бонусов.
- Используется Web Audio API для генерации простых звуков выстрелов и попаданий с изменяемой частотой.
- Анимация выстрела и эффект взрыва выполнены через CSS и JS с плавными переходами и временными задержками.
- Состояние кнопок активируется/деактивируется в зависимости от хода игры.
- Тексты и числа обновляются динамически в DOM.
- Обработчики событий для кнопок, изменения настроек и окна ресайза.
- Автоматический игровой цикл (
requestAnimationFrame) для плавного движения целей. - Игра автоматически подстраивается под ширину игрового поля при изменении размеров окна, корректируя зоны появления целей.
- Используется единый массив
targets, где каждый объект содержит DOM-элемент и параметры движения, что упрощает управление множеством целей. - Таймеры
setTimeoutиsetIntervalприменяются для появления и исчезновения целей, обновления времени и повышения уровня. - Звуковые эффекты генерируются динамически с изменением частоты в зависимости от комбо, что даёт вариативность аудио-отклика.
- Продуманное разделение кода: функции для создания, отображения, скрытия и движения целей; управление миссиями, обработка выстрелов и столкновений; логика таймера и уровней.
- Кнопки управления динамически включаются и выключаются для предотвращения ошибок в состоянии паузы или после окончания игры.
- Есть плавные анимации появления целей, выстрелов и взрывов для визуальной привлекательности.
- Интерфейс и тексты содержат понятные подсказки и состояние игры, включая отображение сложности и миссий.
- Хорошая читаемость и поддерживаемость кода для расширения или модификации игры.
Подробный разбор кода
1. Инициализация и настройка
- Получение DOM-элементов для игрового поля, кнопок управления и отображения информации (
scoreDisplay,timerDisplayи др.). - Объявление глобальных переменных состояния игры: счёт, время, комбо, уровень, миссия, массив целей, флаг работы игры и параметры сложности.
- Объект
difficultySettingsсодержит стартовое количество целей и множитель скорости для каждого уровня сложности (easy,medium,hard). - Функция
updateGameAreaDimensionsобновляет размеры игрового поля при изменении размера окна. - Функция
createTarget(type)создаёт DOM-элемент цели (div) с классомnormalилиfastв зависимости от типа, задаёт размеры и начальные параметры позиционирования и движения. Возвращает объект цели со свойствами для управления. createTargets()очищает предыдущие цели, удаляя DOM-элементы и таймеры, затем создаёт новые цели исходя из текущего уровня и сложности: сначала обычные, потом быстрые. Количество быстрых целей растёт с уровнем.showTarget(target)случайным образом позиционирует цель в пределах игрового поля с учётом отступов, задаёт скорости движения с усилением в зависимости от уровня и сложности, активирует цель и устанавливает таймер автоматического скрытия и возрождения после случайной задержки.hideTarget(target)делает цель неактивной и скрывает её из видимости.moveTargets()вызывается в игровом цикле (requestAnimationFrame), обновляет координаты активных целей при движении, отражая их от границ поля.- Функция
shoot()создаёт визуальный объект снаряда в нижней части игрового поля под кнопкой Выстрел, запускает анимацию подъёма снаряда. - Во время анимации происходит проверка пересечения с каждым активным объектом цели через функцию
isCollide, используя прямоугольные границы элементов (getBoundingClientRect). - При попадании в цель счёт увеличивается, комбо растёт, максимальное комбо обновляется, начисляются бонусные очки за каждую третью серию.
- Если цель была типа
fast, увеличивается счётчик попаданий для текущей миссии. - Воспроизводятся звуковые эффекты с частотой, зависящей от текущего комбо, что усиливает игровой отклик.
- Создаётся визуальный эффект взрыва (
createExplosion), цель скрывается и запускается таймер возрождения. - Если снаряд не попал в цель и вышел за пределы поля, то удаляется, а комбо сбрасывается.
- При запуске и повышении уровня вызывается функция
generateMission(), которая создаёт миссию – убить заданное количество быстрых целей (fastTargets). Количество целей растёт с уровнем. - Функция
checkMission()отслеживает выполнение миссии, меняет статус миссии и увеличивает счёт бонусом, если задача выполнена. - Таймер запускается функцией
startTimer(), которая отсчитывает выбранное в настройках время, обновляя текущее значение каждую секунду. - При достижении нуля вызывается функция
endGame(), которая останавливает игру. - Большие уровни меняются автоматически каждые 30 секунд, вызывая
levelUp(), которая увеличивает счёт количества целей, создаёт новые и задаёт новую миссию. - Кнопка
startBtnначинает игру, устанавливая начальные параметры и запуская основные процессы: создание целей, запуск таймера, активация кнопок. - Кнопка
restartBtnзапускает игру заново с текущими настройками. - Завершение игры останавливает движения и обновления, выводит итоговую статистику с учётом бонусов.
- Используется Web Audio API для создания звуковых эффектов с параметрами частоты и громкости, задаваемыми программно с учётом текущего комбо.
- Визуальные эффекты снарядов и взрывов реализованы на CSS с анимацией прозрачности и масштабирования.
- Обработчики кнопок запуска, выстрела, перезапуска, а также изменения настроек длительности и сложности.
- Обработка ресайза окна для адаптации размеров игрового поля и корректного позиционирования целей.
- Бесконечный игровой цикл
gameLoopс использованиемrequestAnimationFrameдля плавного перемещения целей.
Полный код игры:
game1.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Кликер с миссиями и настройками</title>
<style>
html, body {
height: 100%;
overflow-y: auto;
margin: 0;
padding: 0;
}
body {
background: #121212;
color: #eee;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px 10px 40px;
user-select: none;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
margin-bottom: 10px;
text-align: center;
font-size: 1.6rem;
}
/* Стили для нового блока правил */
#rules {
width: 90vw;
max-width: 360px;
background: #1a1a1a;
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 0 15px #000;
color: #ccc;
box-sizing: border-box;
font-size: 0.9rem;
line-height: 1.4;
}
#rules h2 {
color: #ffbb33;
font-size: 1.1rem;
margin: 0 0 10px;
text-align: center;
}
#rules ul {
padding-left: 20px;
margin: 0;
}
#rules li {
margin-bottom: 5px;
}
#gameArea {
position: relative;
width: 90vw;
max-width: 360px;
aspect-ratio: 3 / 5;
max-height: 600px;
min-height: 300px;
background: #222;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 0 15px #000;
margin: 0 auto 15px;
box-sizing: border-box;
}
#gameArea > div, .projectile, .target, .explosion {
position: absolute;
}
#startBtn, #fireBtn, #restartBtn {
display: block;
margin: 15px auto 10px;
padding: 14px 40px;
font-size: 24px;
border-radius: 14px;
border: none;
color: white;
cursor: pointer;
user-select: none;
min-width: 140px;
max-width: 240px;
width: 90vw;
max-width: 360px;
box-shadow: 0 0 12px;
}
#startBtn { background-color: #007bff; }
#fireBtn { background-color: #ff4a00; }
#fireBtn:disabled { background-color: #555; cursor: not-allowed; }
#restartBtn { background-color: #28a745; display: none; }
.projectile {
width: 12px;
height: 24px;
background: linear-gradient(to top, #ff9e00, #ff3700);
border-radius: 6px;
bottom: 60px;
z-index: 20;
pointer-events: none;
}
.target {
border-radius: 50%;
box-shadow: 0 0 20px #00ff99;
opacity: 0;
transition: opacity 0.4s ease;
z-index: 10;
background-color: #00ff99 !important;
}
.target.fast {
background-color: #ff5555 !important;
box-shadow: 0 0 25px #ff5555;
}
.target.show { opacity: 1; }
.explosion {
border-radius: 50%;
pointer-events: none;
animation: explodeAnim 0.4s forwards;
background: radial-gradient(circle, #ffcc00, #ff6600);
}
@keyframes explodeAnim {
0% { opacity: 1; transform: translate(0, 0) scale(1); }
100% { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(0); }
}
#score, #timer, #combo, #level, #mission, #difficultyDisplay {
font-size: 1.4rem;
margin: 5px 0;
text-align: center;
max-width: 360px;
width: 90vw;
}
#difficultyDisplay { color: #ffbb33; margin-bottom: 10px; }
#result { color: #ffbb33; font-size: 1.6rem; margin-top: 20px; text-align: center; }
#settings {
width: 90vw;
max-width: 360px;
background: #1a1a1a;
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 0 15px #000;
color: #ccc;
box-sizing: border-box;
}
</style>
</head>
<body>
<h1>Кликер с миссиями</h1>
<!-- Твой новый блок с правилами -->
<div id="rules">
<h2>Как играть:</h2>
<ul>
<li>Цель: сбивать движущиеся мишени.</li>
<li><b>Зелёные мишени:</b> обычные, дают 1 очко.</li>
<li><b>Красные мишени:</b> быстрые, нужны для миссии.</li>
<li><b>Комбо:</b> каждое попадание без промаха увеличивает комбо.</li>
<li>Управление: кнопка «Выстрелить» или клавиша <b>Пробел</b>.</li>
<li>Миссия: сбей нужное число красных мишеней для бонуса.</li>
</ul>
</div>
<div id="settings">
<label>Время (сек):</label>
<select id="durationSelect"><option value="15">15</option><option value="30" selected>30</option><option value="60">60</option></select>
<label>Сложность:</label>
<select id="difficultySelect"><option value="easy">Лёгкий</option><option value="medium" selected>Средний</option><option value="hard">Сложный</option></select>
</div>
<div id="score">Очки: 0</div>
<div id="timer">Время: 30</div>
<div id="combo">Комбо: 0</div>
<div id="level">Уровень: 1</div>
<div id="difficultyDisplay">Сложность: Средний</div>
<div id="mission">Миссия: Подготовка...</div>
<div id="gameArea"></div>
<button id="startBtn">Начать игру</button>
<button id="fireBtn" disabled>Выстрелить!</button>
<div id="result"></div>
<button id="restartBtn">Сыграть заново</button>
<script>
// Весь твой JS без изменений
window.addEventListener('DOMContentLoaded', () => {
const gameArea = document.getElementById('gameArea');
const startBtn = document.getElementById('startBtn');
const fireBtn = document.getElementById('fireBtn');
const restartBtn = document.getElementById('restartBtn');
const scoreDisplay = document.getElementById('score');
const timerDisplay = document.getElementById('timer');
const comboDisplay = document.getElementById('combo');
const levelDisplay = document.getElementById('level');
const difficultyDisplay = document.getElementById('difficultyDisplay');
const missionDisplay = document.getElementById('mission');
const resultDisplay = document.getElementById('result');
const durationSelect = document.getElementById('durationSelect');
const difficultySelect = document.getElementById('difficultySelect');
let gameWidth, gameHeight, score, timeLeft, combo, maxCombo, gameRunning, targets, level, mission;
const difficultySettings = {
easy: {maxTargetsStart: 3, speedMultiplier: 0.7},
medium: {maxTargetsStart: 5, speedMultiplier: 1},
hard: {maxTargetsStart: 7, speedMultiplier: 1.5},
};
const ctx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, duration=100) {
if(ctx.state === 'suspended') ctx.resume();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();
osc.type = 'triangle';
osc.frequency.value = freq;
gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration/1000);
osc.connect(gainNode);
gainNode.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + duration/1000);
}
function updateGameAreaDimensions() {
gameWidth = gameArea.clientWidth;
gameHeight = gameArea.clientHeight;
}
function createTarget(type='normal') {
const size = type==='fast' ? 35 : 50;
const t = document.createElement('div');
t.className = 'target ' + type;
t.style.width = size + 'px';
t.style.height = size + 'px';
gameArea.appendChild(t);
return {el: t, x: 0, y: 0, size, type, active: false, speedX: 0, speedY:0, disappearTimeout: null};
}
function createTargets() {
updateGameAreaDimensions();
if (targets) targets.forEach(t => { clearTimeout(t.disappearTimeout); t.el.remove(); });
targets = [];
const max = difficultySettings[difficultySelect.value].maxTargetsStart;
for(let i=0; i < max; i++) targets.push(createTarget(i % 2 === 0 ? 'normal' : 'fast'));
}
function showTarget(target) {
if(!gameRunning) return;
target.x = Math.random() * (gameWidth - target.size - 10) + 5;
target.y = Math.random() * (gameHeight - 200) + 50;
target.el.style.left = target.x + 'px';
target.el.style.top = target.y + 'px';
target.el.style.opacity = "1";
target.active = true;
const baseSpeed = target.type === 'fast' ? 5 : 2;
const mult = difficultySettings[difficultySelect.value].speedMultiplier;
target.speedX = (Math.random() * baseSpeed + 1) * mult * (Math.random() > 0.5 ? 1 : -1);
target.speedY = (Math.random() * baseSpeed + 1) * mult * (Math.random() > 0.5 ? 1 : -1);
target.disappearTimeout = setTimeout(() => {
target.active = false;
target.el.style.opacity = "0";
setTimeout(() => showTarget(target), 1000);
}, 3000);
}
function createExplosion(x, y, size = 50) {
for (let i = 0; i < 10; i++) {
const spark = document.createElement('div');
spark.className = 'explosion';
const sSize = Math.random() * 8 + 4;
spark.style.width = sSize + 'px';
spark.style.height = sSize + 'px';
spark.style.left = (x + size / 2) + 'px';
spark.style.top = (y + size / 2) + 'px';
spark.style.setProperty('--tx', (Math.random() - 0.5) * 150 + 'px');
spark.style.setProperty('--ty', (Math.random() - 0.5) * 150 + 'px');
gameArea.appendChild(spark);
setTimeout(() => spark.remove(), 400);
}
}
function shoot() {
if(!gameRunning) return;
const proj = document.createElement('div');
proj.className = 'projectile';
const leftPos = gameWidth / 2 - 6;
proj.style.left = leftPos + 'px';
proj.style.bottom = '60px';
gameArea.appendChild(proj);
playSound(600, 80);
let posY = 60;
function update() {
posY += 15;
proj.style.transform = `translateY(-${posY}px)`;
const pRect = proj.getBoundingClientRect();
for(const t of targets) {
if(t.active) {
const tRect = t.el.getBoundingClientRect();
if(!(pRect.right < tRect.left || pRect.left > tRect.right || pRect.bottom < tRect.top || pRect.top > tRect.bottom)) {
score++; combo++;
if(t.type === 'fast') mission.fastTargetsHit++;
scoreDisplay.textContent = `Очки: ${score}`;
comboDisplay.textContent = `Комбо: ${combo}`;
createExplosion(t.x, t.y, t.size);
playSound(800 + combo * 20, 100);
t.active = false; t.el.style.opacity = "0";
clearTimeout(t.disappearTimeout);
setTimeout(() => showTarget(t), 500);
proj.remove(); return;
}
}
}
if(posY > gameHeight + 50) { proj.remove(); combo = 0; comboDisplay.textContent = `Комбо: 0`; }
else requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
function startGame() {
// Добавляем активацию звука:
if (ctx && ctx.state === 'suspended') ctx.resume();
score = 0; combo = 0; level = 1; gameRunning = true;
scoreDisplay.textContent = 'Очки: 0';
resultDisplay.textContent = '';
startBtn.style.display = 'none';
restartBtn.style.display = 'none';
fireBtn.disabled = false;
updateGameAreaDimensions();
createTargets();
targets.forEach(t => showTarget(t));
mission = { goal: 5, fastTargetsHit: 0 };
missionDisplay.textContent = `Миссия: Убей ${mission.goal} красных`;
timeLeft = parseInt(durationSelect.value);
const timer = setInterval(() => {
timeLeft--;
timerDisplay.textContent = `Время: ${timeLeft}`;
if(mission.fastTargetsHit >= mission.goal) missionDisplay.textContent = "Миссия выполнена! (+5 очков)";
if(timeLeft <= 0) {
clearInterval(timer);
gameRunning = false;
fireBtn.disabled = true;
resultDisplay.textContent = `Конец! Счёт: ${score + (mission.fastTargetsHit >= mission.goal ? 5 : 0)}`;
restartBtn.style.display = 'block';
}
}, 1000);
}
startBtn.addEventListener('click', startGame);
restartBtn.addEventListener('click', startGame);
fireBtn.addEventListener('click', shoot);
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if(gameRunning) shoot(); else if(startBtn.style.display !== 'none') startGame();
}
});
function loop() { if(gameRunning) {
targets.forEach(t => {
if(t.active) {
t.x += t.speedX; t.y += t.speedY;
if(t.x < 0 || t.x > gameWidth - t.size) t.speedX *= -1;
if(t.y < 0 || t.y > gameHeight - 150) t.speedY *= -1;
t.el.style.left = t.x + 'px'; t.el.style.top = t.y + 'px';
}
});
} requestAnimationFrame(loop); }
loop();
});
</script>
</body>
</html>
Платформер с уровнями сложности
Описание игры
Это классический 2D-платформер, в котором игрок управляет персонажем, преодолевая препятствия и избегая врагов на пути к цели. Игра требует точности движений и быстроты реакции. Игровой процесс разделён на уровни сложности, каждый из которых предлагает уникальные карты и временные ограничения. Особенностью игры является наличие анимированных эффектов (глаза персонажа, пыль при прыжках) и плавного управления, адаптированного как для клавиатуры, так и для мобильных устройств.
Возможности игры
- Разные уровни сложности: Лёгкий, Средний и Сложный. С ростом сложности увеличивается количество врагов и их скорость, а время на прохождение уровня сокращается.
- Динамические враги: Противники (красные блоки) перемещаются по платформам, пульсируя и меняя направление при достижении границ своего маршрута.
- Визуальные эффекты: Система частиц (пыль) при прыжках и анимированные глаза персонажа, которые смотрят в сторону движения.
- Двойное управление: Полная поддержка клавиатуры (Стрелки, WASD, Пробел) и экранных сенсорных кнопок для мобильных браузеров.
- Таймер прохождения: Ограничение времени на каждый уровень, заставляющее игрока действовать быстро.
- Звуковой отклик: Использование Web Audio API для генерации звуков прыжков, столкновений, победы и окончания времени.
- Адаптивная графика: Канвас автоматически масштабируется под размеры экрана пользователя.
Подробный разбор кода
1. Физика и движение
- Гравитация: Постоянная переменная
gravity, которая каждую итерацию цикла увеличивает вертикальную скорость персонажа (dy). - Коллизии (столкновения): Функция
rectsCollideпроверяет пересечение прямоугольника игрока с платформами, врагами и целью. При падении на платформу вертикальная скорость обнуляется, а персонаж получает статусgrounded. - Delta Time (dt): Расчёт времени прохождения уровня не завязан на кадрах (FPS), а использует разницу времени между кадрами (
Date.now()), что гарантирует одинаковую скорость таймера на любых мониторах.
2. Рендеринг и анимация
- Игровой цикл: Используется
requestAnimationFrameдля плавного обновления картинки. - Отрисовка: На каждый кадр канвас очищается, после чего последовательно рисуются платформы, цель, враги (с расчётом пульсации через
Math.sin) и сам игрок. - Система частиц: Функция
createDustдинамически создает DOM-элементы, которые анимируются независимо от игрового цикла черезsetIntervalи самоудаляются при достижении нулевой прозрачности.
3. Звуковое сопровождение
- Web Audio API: Звуки генерируются программно (без загрузки аудиофайлов). Создается осциллятор (
oscillator), настраивается тип волны (sine,sawtooth,square) и частота, что позволяет создавать уникальные эффекты для каждого игрового события.
4. Управление и интерфейс
- Сенсоры: Обработка событий
touchstart/touchendс отменой стандартного поведения браузера (preventDefault), что убирает задержки и паразитный скролл страницы во время игры. - Состояние игры: Переменная
gameStartedблокирует логику до нажатия кнопки "Начать игру", что необходимо для соблюдения политик автовоспроизведения звука в браузерах.
Полный код игры:
game2.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Платформер с уровнями сложности</title>
<style>
body {
margin: 0; padding: 0;
background: #222;
font-family: Arial, sans-serif;
color: #eee;
text-align: center;
user-select: none;
overflow-x: hidden;
}
h1 {
margin: 20px 0 10px;
font-size: 1.5rem;
}
#rules {
max-width: 600px;
margin: 5px auto 10px;
padding: 10px 15px;
background: #333;
border-radius: 12px;
box-shadow: 0 0 15px #000;
font-size: 14px;
line-height: 1.4;
}
#gameControls {
margin: 10px auto 15px;
max-width: 700px;
}
select {
padding: 8px 12px;
font-size: 16px;
border-radius: 8px;
border: none;
background: #444;
color: #eee;
cursor: pointer;
}
#infoPanel {
margin-top: 6px;
font-size: 18px;
display: none;
justify-content: center;
gap: 20px;
}
canvas {
background: #333;
display: none;
margin: 15px auto 5px;
border: 3px solid #555;
border-radius: 12px;
max-width: 95vw;
height: auto;
touch-action: none;
}
#mobileControls {
display: none;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
}
.btnControl {
width: 70px;
height: 70px;
background: #555;
border-radius: 50%;
box-shadow: 0 0 15px #000;
font-size: 24px;
color: #eee;
line-height: 70px;
text-align: center;
cursor: pointer;
touch-action: none;
}
.btnControl.pressed {
background: #ff7f50;
box-shadow: 0 0 25px #ff7f50;
}
#startBtn {
margin: 20px auto 30px;
display: inline-block;
padding: 14px 40px;
font-size: 24px;
font-weight: 700;
border-radius: 14px;
background: #ff4a00;
color: #fff;
cursor: pointer;
box-shadow: 0 0 20px #ff4a00;
border: none;
}
.p-dust {
position: fixed;
background: rgba(255,255,255,0.6);
pointer-events: none;
border-radius: 50%;
z-index: 100;
}
</style>
</head>
<body>
<h1>Платформер с уровнями сложности</h1>
<div id="rules">
<strong>Правила:</strong>
<ul style="text-align:left; padding-left: 20px; margin: 5px 0;">
<li>Стрелки или кнопки для движения, Пробел или кнопка вверх для прыжка.</li>
<li>Цель — зелёный квадрат. Враги — красные пульсирующие блоки.</li>
<li>Касание врага или конец времени — перезапуск уровня.</li>
</ul>
</div>
<div id="gameControls">
Сложность:
<select id="difficultySelect">
<option value="easy">Лёгкий</option>
<option value="medium" selected>Средний</option>
<option value="hard">Сложный</option>
</select>
</div>
<div id="infoPanel">
<div id="levelDisplay">Уровень: 1</div>
<div id="score">Очки: 0</div>
<div id="timer">Время: 0</div>
</div>
<canvas id="gameCanvas" width="700" height="400"></canvas>
<div id="mobileControls">
<div id="btnLeft" class="btnControl">◄</div>
<div id="btnJump" class="btnControl">▲</div>
<div id="btnRight" class="btnControl">►</div>
</div>
<button id="startBtn">Начать игру</button>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElem = document.getElementById('score');
const levelDisplay = document.getElementById('levelDisplay');
const timerElem = document.getElementById('timer');
const difficultySelect = document.getElementById('difficultySelect');
const startBtn = document.getElementById('startBtn');
const infoPanel = document.getElementById('infoPanel');
const mobileControls = document.getElementById('mobileControls');
const gravity = 0.5;
let score = 0;
let currentLevel = 0;
let gameStarted = false;
let timeLeft = 0;
let lastTime = 0;
// Звуковой контекст
let audioCtx = null;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playSound(freq, type = 'sine', duration = 0.1) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
const levelsData = {
easy: [
{
platforms: [{x:0, y:350, width:700, height:50}, {x:150, y:280, width:120, height:15}, {x:350, y:230, width:120, height:15}, {x:550, y:180, width:120, height:15}],
goal: {x: 620, y:130, width:30, height:30, color:'#0f0'},
enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}}],
time: 90
},
{
platforms: [{x:0, y:350, width:700, height:50}, {x:100, y:300, width:150, height:15}, {x:350, y:250, width:150, height:15}, {x:550, y:200, width:120, height:15}],
goal: {x:620, y:170, width:30, height:30, color:'#0f0'},
enemies: [{x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:450}}],
time: 85
}
],
medium: [
{
platforms: [{x:0, y:350, width:700, height:50}, {x:150, y:280, width:120, height:15}, {x:350, y:230, width:120, height:15}, {x:550, y:180, width:120, height:15}],
goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}}, {x:400, y:195, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:400,max:650}}],
time: 80
},
{
platforms: [{x:0, y:350, width:700, height:50}, {x:100, y:300, width:150, height:15}, {x:350, y:250, width:150, height:15}, {x:550, y:200, width:150, height:15}],
goal: {x:620, y:150, width:30, height:30, color:'#0f0'},
enemies: [{x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:400}}, {x:550, y:165, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:550,max:680}}],
time: 75
}
],
hard: [
{
platforms: [{x:0, y:350, width:700, height:50}, {x:130, y:300, width:140, height:15}, {x:350, y:250, width:140, height:15}, {x:550, y:190, width:140, height:15}],
goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:10,max:650}}, {x:420, y:215, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:420,max:650}}, {x:560, y:155, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:560,max:680}}],
time: 60
},
{
platforms: [{x:0, y:350, width:700, height:50}, {x:120, y:320, width:80, height:15}, {x:280, y:270, width:100, height:15}, {x:430, y:220, width:110, height:15}, {x:600, y:170, width:90, height:15}],
goal: {x:650, y:120, width:30, height:30, color:'#0f0'},
enemies: [{x:120, y:305, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:120,max:200}}, {x:280, y:255, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:280,max:380}}, {x:430, y:205, width:40, height:35, color:'#f00', speed:6, direction:1, range:{min:430,max:540}}, {x:600, y:155, width:40, height:35, color:'#f00', speed:7, direction:1, range:{min:600,max:690}}],
time: 50
}
]
};
const player = { x: 50, y: 0, width: 30, height: 50, color: '#ff9933', dy: 0, dx: 0, speed: 4, jumpStrength: 12, grounded: false };
let platforms = [], goal = null, enemies = [];
const keys = { left: false, right: false };
function rectsCollide(r1, r2) {
return !(r1.x > r2.x + r2.width || r1.x + r1.width < r2.x || r1.y > r2.y + r2.height || r1.y + r1.height < r2.y);
}
function update() {
if(!gameStarted) return;
// Расчет дельты времени для таймера
let now = Date.now();
let dt = (now - lastTime) / 1000;
lastTime = now;
timeLeft -= dt;
timerElem.textContent = 'Время: ' + Math.ceil(Math.max(0, timeLeft));
if(timeLeft <= 0) {
playSound(150, 'sawtooth', 0.3);
alert('Время вышло!');
loadLevel(currentLevel);
return;
}
player.dx = 0;
if(keys.left) player.dx = -player.speed;
if(keys.right) player.dx = player.speed;
player.x += player.dx;
player.dy += gravity;
player.y += player.dy;
player.grounded = false;
// Коллизии с платформами
for(let platform of platforms){
if(player.x < platform.x + platform.width && player.x + player.width > platform.x &&
player.y + player.height >= platform.y && player.y + player.height <= platform.y + platform.height && player.dy >= 0) {
player.y = platform.y - player.height;
player.dy = 0;
player.grounded = true;
}
}
// Границы канваса
if(player.x < 0) player.x = 0;
if(player.x + player.width > canvas.width) player.x = canvas.width - player.width;
if(player.y + player.height > canvas.height){
player.y = canvas.height - player.height;
player.dy = 0;
player.grounded = true;
}
// Враги
for(let enemy of enemies){
enemy.x += enemy.speed * enemy.direction;
if(enemy.x > enemy.range.max || enemy.x < enemy.range.min) enemy.direction *= -1;
if(rectsCollide(player, enemy)){
playSound(100, 'square', 0.2);
alert('Враг тебя поймал!');
loadLevel(currentLevel);
return;
}
}
// Цель
if(rectsCollide(player, goal)){
playSound(800, 'sine', 0.4);
score++;
scoreElem.textContent = 'Очки: ' + score;
currentLevel++;
if(currentLevel >= levelsData[difficultySelect.value].length){
alert('Победа! Все уровни пройдены!');
currentLevel = 0;
score = 0;
}
loadLevel(currentLevel);
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#999';
for(let platform of platforms) ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
ctx.fillStyle = goal.color;
ctx.fillRect(goal.x, goal.y, goal.width, goal.height);
for(let enemy of enemies){
ctx.fillStyle = enemy.color;
let pulse = Math.sin(Date.now()/150) * 3;
ctx.fillRect(enemy.x, enemy.y - pulse, enemy.width, enemy.height + pulse);
}
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);
ctx.fillStyle = 'white';
let eyeOffset = keys.right ? 18 : (keys.left ? 2 : 10);
ctx.fillRect(player.x + eyeOffset, player.y + 10, 5, 5);
ctx.fillRect(player.x + eyeOffset + 7, player.y + 10, 5, 5);
}
function jump() {
if(player.grounded) {
playSound(400, 'sine', 0.1);
player.dy = -player.jumpStrength;
for(let i=0; i<5; i++) createDust(player.x + player.width/2, player.y + player.height);
}
}
function createDust(x, y) {
const d = document.createElement('div');
d.className = 'p-dust';
const size = Math.random() * 6 + 2;
d.style.width = size + 'px';
d.style.height = size + 'px';
const rect = canvas.getBoundingClientRect();
d.style.left = (rect.left + (x * (rect.width/canvas.width))) + 'px';
d.style.top = (rect.top + (y * (rect.height/canvas.height))) + 'px';
document.body.appendChild(d);
let sx = (Math.random() - 0.5) * 4, sy = Math.random() * -2, op = 1;
let anim = setInterval(() => {
op -= 0.05; sy += 0.1;
d.style.left = (parseFloat(d.style.left) + sx) + 'px';
d.style.top = (parseFloat(d.style.top) + sy) + 'px';
d.style.opacity = op;
if(op <= 0) { clearInterval(anim); d.remove(); }
}, 30);
}
startBtn.addEventListener('click', () => {
initAudio();
startBtn.style.display = 'none';
canvas.style.display = 'block';
infoPanel.style.display = 'flex';
mobileControls.style.display = 'flex';
loadLevel(0);
lastTime = Date.now();
gameLoop();
});
function gameLoop() {
if(gameStarted) {
update();
draw();
requestAnimationFrame(gameLoop);
}
}
function loadLevel(index) {
const lvl = levelsData[difficultySelect.value][index];
platforms = [...lvl.platforms];
goal = {...lvl.goal};
enemies = lvl.enemies ? lvl.enemies.map(e => ({...e})) : [];
player.x = 50; player.y = 0; player.dy = 0; player.grounded = false;
timeLeft = lvl.time;
lastTime = Date.now();
levelDisplay.textContent = 'Уровень: ' + (index+1);
gameStarted = true;
}
// Управление
document.addEventListener('keydown', e => {
if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if(e.code === 'ArrowUp' || e.code === 'Space' || e.code === 'KeyW') jump();
});
document.addEventListener('keyup', e => {
if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
});
function bind(btn, key) {
const start = (e) => { e.preventDefault(); if(key==='jump') jump(); else keys[key]=true; btn.classList.add('pressed'); };
const end = (e) => { e.preventDefault(); if(key!=='jump') keys[key]=false; btn.classList.remove('pressed'); };
btn.addEventListener('touchstart', start); btn.addEventListener('touchend', end);
btn.addEventListener('mousedown', start); btn.addEventListener('mouseup', end);
}
bind(document.getElementById('btnLeft'), 'left');
bind(document.getElementById('btnRight'), 'right');
bind(document.getElementById('btnJump'), 'jump');
window.addEventListener('resize', () => {
const ratio = 700 / 400;
let w = Math.min(window.innerWidth * 0.95, 700);
canvas.style.width = w + 'px';
canvas.style.height = (w / ratio) + 'px';
});
window.dispatchEvent(new Event('resize'));
</script>
</body>
</html>
Музыкальный реактор с секвенсером
Описание игры
Это интерактивный музыкальный секвенсер, где пользователь нажимает нотные кнопки, чтобы услышать ноты в режиме реального времени. Можно создавать и редактировать ритмические паттерны, активируя шаги в 16-таймном секвенсере для каждой ноты. Запускать последовательность звуков можно кнопкой "Старт" с возможностью регулировки темпа. Игра позволяет экспериментировать с музыкальными сочетаниями, создавая зацикленные мелодии в браузере с простой, но функциональной визуализацией и приятным дизайном.
Возможности игры
- Нотные кнопки: 9 небольших кнопок с нотами (C4–D5), которые можно нажимать для мгновенного воспроизведения ноты.
- Секвенсер: сетка 9 строк (по нотам) и 16 шагов (по времени), в которой можно включать/выключать каждый шаг и создавать паттерны.
- Синхронизация проигрывания: последовательное проигрывание активных шагов с подсветкой текущего шага.
- Регулировка темпа: слайдер от 60 до 200 BPM, с отображением текущего значения.
- Управление проигрыванием: кнопки "Старт" и "Стоп" для запуска и остановки воспроизведения.
- Доступность: элементы управления доступны с клавиатуры (Tab, Enter, пробел), для удобства навигации.
- Звуковое оформление: воспроизведение простого синусоидального звучания нот через Web Audio API.
- Анимации и визуальная отдача: подсветка нажимаемых нот и активных шагов секвенсера.
- Адаптивный дизайн: игра корректно отображается на разных размерах экранов с удобной версткой.
- Создаётся экземпляр
AudioContextдля воспроизведения звуков. - Определяется частота каждой ноты в объекте
notesFreqдля использования в звуковом синтезе. - Выбираются и сохраняются в переменные кнопки нот (
.pad), секвенсер (#sequencer), контролы управления (#startBtn,#stopBtn, слайдер#tempo) и индикатор темпа. - Устанавливаются параметры: количество шагов в секвенсере (
stepsCount = 16), массив нот (noteKeys), данные активных шаговsequencerData, флаги воспроизведения и текущий шаг. - Для каждой кнопки ноты навешиваются обработчики клика и клавиатуры (Enter, пробел).
- При нажатии вызывается функция
playNote(note), которая включает генератор синусоиды в аудио-контексте с заданной частотой. Нота звучит около 280 мс с экспоненциальным спадом громкости. - Функция
createSequencer()создаёт сетку из 9 строк по 16 шагов. - Каждый шаг – это
divс классомstep, табуляцией и aria-метками для доступности. - Обработчики клика и клавиатуры переключают класс
active, обновляют массивsequencerData, который хранит состояние шагов (включён/выключен). - Функция
playStep()отвечает за последовательное проигрывание текущего шага: - Вычисляется интервал воспроизведения шагов на основе значения темпа (BPM).
- Подсвечиваются активные шаги текущего шага, снимается подсветка с предыдущих.
- Проигрываются все ноты, активные в текущем шаге, вызывая
playNote(). - Текущий шаг увеличивается с переходом на начало после 16-го.
- Запускается таймер для следующего вызова
playStep. - Кнопка
startBtnзапускает проигрывание, но только если включён хотя бы один шаг в секвенсере, иначе выводит предупреждение. - Кнопка
stopBtnостанавливает проигрывание, сбрасывая подсветки и таймеры. - При изменении значения темпа слайдером обновляется отображение и, если идёт проигрывание, пересчитывается скорость воспроизведения.
- Все интерактивные элементы поддерживают навигацию с клавиатуры (Tab, Enter, пробел).
- Для визуальной обратной связи используются CSS-анимации подсветки и изменения фона.
- Верстка сетки и кнопок выполнена через CSS Grid и Flexbox, что обеспечивает адаптивность.
- Используется Web Audio API с осциллятором типа
sineи экспоненциальным снижением громкости для приятного звучания. - Состояние секвенсера хранится в двумерном массиве
sequencerData, позволяющем быстро управлять включёнными шагами. - Воспроизведение нот осуществляется циклично, с реализацией плавного переключения и синхронизации шагов.
- Управление скоростью воспроизведения реализовано через таймер
setTimeout, пересчитывающий интервал при смене темпа. - Все элементы обладают aria-атрибутами для лучшей доступности.
- Применены эффекты визуальной подсветки для активных шагов и нажатий нот, что улучшает восприятие и удобство.
- В дизайне учтена поддержка касаний и пользовательских вводов на мобильных устройствах.
- Кнопки и слайдеры ограничены в управлении (например, кнопка "Стоп" недоступна если игра не играет), предотвращая ошибки.
- Хорошо структурированный и легко читаемый код позволяет расширять функционал и адаптировать игру под разные задачи.
Подробный разбор кода
1. Инициализация и настройка аудио и элементов
- Создаётся экземпляр
AudioContext(audioCtx) для работы со звуком. - Определяется объект
notesFreq, сопоставляющий каждую ноту (C4–D5) с её частотой в Гц. - Получаются DOM-элементы: кнопки нот (
.pad), секвенсер (#sequencer), кнопки управления (startBtn,stopBtn), слайдер темпа (tempoInput), а также элемент для отображения темпа (tempoValue). - Задаются константы: количество шагов в секвенсере (
stepsCount = 16), массив нот (noteKeys), а также переменные для хранения данных секвенсера (sequencerData), состояния воспроизведения (playing) и текущего шага (currentStep). - Для каждой нотной кнопки (
pad) навешиваются обработчики клика и клавиатурных событий (Enter, пробел) – для удобства управления с клавиатуры. - При активации кнопки вызывается
playNote(note): создаётся осциллятор типаsineна частоте, соответствующей ноте, подключаемый к усилителю (GainNode). - Усилитель плавно снижает громкость звука экспоненциально за 280 мс, чтобы звук был мягким и естественным.
- Функция
createSequencer()строит сетку из 9 строк по 16 шагов, соответствующую нотам и временным промежуткам. - Каждый шаг –
divс классомstep, доступностью (табуляция, aria-label) и обработчиками клика и клавиатуры для включения/выключения шага. - Состояния всех шагов хранятся в двумерном массиве
sequencerData(по нотам и шагам). - Функция
playStep()проигрывает текущий шаг секвенсера: - Вычисляется интервал между шагами на основе темпа (формула:
(60 / BPM) / 4 * 1000мс). - Удаляется подсветка с предыдущих шагов, подсвечиваются активные шаги текущего индекса.
- Для всех включённых шагов текущего шага вызывается
playNoteс соответствующими нотами. - Индекс текущего шага увеличивается с циклом до 16.
- Запускается таймер
setTimeoutдля следующего вызоваplayStep. - Кнопка «Старт» запускает проигрывание, если хотя бы один шаг активен, иначе выдаёт предупреждение. Включается режим воспроизведения, кнопка «Старт» блокируется, «Стоп» становится активной.
- Кнопка «Стоп» останавливает проигрывание, сбрасывает подсветки и удаляет таймер. Кнопки меняют состояние активности.
- При изменении значения ползунка темпа обновляется отображение BPM и, если игра идёт, пересчитывается скорость проигрывания.
- Все интерактивные элементы имеют tabindex и aria-метки для удобного управления с клавиатуры и экранных читалок.
- Кнопки и шаги умеют подсвечиваться при активности, создавая визуальный отклик.
- Сетки созданы с помощью CSS Grid и Flexbox, что обеспечивает адаптивное отображение на разных экранах и устройствах.
- Инициализация AudioContext: Чтобы избежать блокировки звука в мобильных браузерах (Safari, Chrome), вызов функции
initAudio()привязан к событию «Старт». Это гарантирует, что аудио-движок активируется только после осознанного взаимодействия пользователя с интерфейсом. - Точность воспроизведения: Интервал между шагами секвенсера динамически пересчитывается в зависимости от темпа (BPM). Использование
setTimeoutвнутри рекурсивной функцииplayStepпозволяет менять скорость проигрывания «на лету» без перезапуска всего цикла. - Анализ частот (Visualizer): В коде используется
AnalyserNodeдля получения данных о громкости в реальном времени. С помощьюrequestAnimationFrameстроится визуализация наcanvas, где высота графических столбцов напрямую зависит от амплитуды звуковых колебаний в данный конкретный момент. - Синтез звука: Вместо использования тяжелых аудиофайлов применяется динамическая генерация волн типа
sine. Каждая нота имеет свой коэффициент затуханияexponentialRampToValueAtTime, что исключает неприятные щелчки при обрыве звучания и делает мелодию мягкой
Полный код игры:
game3.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Музыкальный реактор с секвенсером</title>
<style>
body {
background: #121212;
color: #eee;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 10px 30px;
user-select: none;
min-height: 100vh;
margin: 0;
}
h1 {
margin-bottom: 10px;
font-size: 1.8rem;
text-align: center;
}
#rules {
max-width: 480px;
width: 100%;
background: #222;
padding: 15px;
border-radius: 12px;
box-shadow: 0 0 15px #000;
margin-bottom: 20px;
font-size: 1rem;
line-height: 1.4;
box-sizing: border-box;
}
#rules h2 {
color: #ffbb33;
margin-top: 0;
font-weight: 600;
text-align: center;
}
#v-canvas {
width: 100%;
max-width: 480px;
background: #000;
border-radius: 12px;
margin-bottom: 15px;
border: 2px solid #333;
height: 100px;
}
.pad-container {
display: grid;
grid-template-columns: repeat(9, 1fr);
gap: 10px;
margin-bottom: 20px;
max-width: 480px;
width: 100%;
box-sizing: border-box;
}
.pad {
width: 100%;
aspect-ratio: 1 / 1;
max-width: 60px;
background: #333;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.8rem;
cursor: pointer;
box-shadow: 0 0 10px #000;
transition: background 0.2s, box-shadow 0.2s;
touch-action: manipulation;
}
.pad.active {
background: #ff6f61;
box-shadow: 0 0 20px #ff6f61;
}
#sequencer {
max-width: 480px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px;
}
.sequencer-row {
display: flex;
justify-content: center;
gap: 6px;
margin-bottom: 8px;
}
.step {
flex: 1;
max-width: 30px;
aspect-ratio: 1 / 1;
background: #222;
border-radius: 6px;
cursor: pointer;
box-shadow: 0 0 6px #000;
transition: background 0.2s;
}
.step:nth-child(4n+1) { background: #2a2a2a; }
.step.active {
background: #ff6f61;
box-shadow: 0 0 12px #ff6f61;
}
.step.playing {
border: 2px solid #ffbb33;
box-sizing: border-box;
}
#controls {
margin-top: 10px;
max-width: 480px;
width: 100%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
justify-content: center;
}
#tempo {
flex-grow: 1;
min-width: 150px;
max-width: 250px;
cursor: pointer;
}
button {
background-color: #ff4a00;
border: none;
border-radius: 14px;
padding: 12px 25px;
font-size: 1.1rem;
color: white;
font-weight: 600;
cursor: pointer;
box-shadow: 0 0 14px #ff4a00;
flex: 1 1 120px;
}
button:disabled {
background-color: #555;
box-shadow: none;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>Музыкальный реактор</h1>
<div id="rules">
<h2>Как играть:</h2>
<ul>
<li>Верхние кнопки — ноты в реальном времени.</li>
<li>Сетка ниже — секвенсер. Отмечай шаги для авто-ритма.</li>
<li>Нажми <b>"Старт"</b>, чтобы запустить бесконечный цикл.</li>
</ul>
</div>
<canvas id="v-canvas"></canvas>
<div class="pad-container" id="pads">
<div class="pad" data-note="C4" tabindex="0">C4</div>
<div class="pad" data-note="D4" tabindex="0">D4</div>
<div class="pad" data-note="E4" tabindex="0">E4</div>
<div class="pad" data-note="F4" tabindex="0">F4</div>
<div class="pad" data-note="G4" tabindex="0">G4</div>
<div class="pad" data-note="A4" tabindex="0">A4</div>
<div class="pad" data-note="B4" tabindex="0">B4</div>
<div class="pad" data-note="C5" tabindex="0">C5</div>
<div class="pad" data-note="D5" tabindex="0">D5</div>
</div>
<div id="sequencer"></div>
<div id="controls">
<button id="startBtn">Старт</button>
<button id="stopBtn" disabled>Стоп</button>
<div style="width: 100%; text-align: center;">
<label>Темп: <span id="tempoValue">120</span> BPM</label><br/>
<input type="range" id="tempo" min="60" max="200" value="120" />
</div>
</div>
<script>
const notesFreq = { C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23, G4: 392.00, A4: 440.00, B4: 493.88, C5: 523.25, D5: 587.33 };
const noteKeys = Object.keys(notesFreq);
let audioCtx = null;
let analyser = null;
let playing = false;
let currentStep = 0;
let nextStepTimeout = null;
const stepsCount = 16;
let sequencerData = noteKeys.map(() => Array(stepsCount).fill(false));
const canvas = document.getElementById('v-canvas');
const vCtx = canvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const tempoInput = document.getElementById('tempo');
const tempoValue = document.getElementById('tempoValue');
const sequencerDiv = document.getElementById('sequencer');
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
analyser.connect(audioCtx.destination);
drawVisualizer();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playNote(freq) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
osc.connect(gain);
gain.connect(analyser);
osc.start();
osc.stop(audioCtx.currentTime + 0.4);
}
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
vCtx.fillStyle = '#000';
vCtx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const h = dataArray[i] / 2;
vCtx.fillStyle = `rgb(255, ${dataArray[i] + 100}, 50)`;
vCtx.fillRect(x, canvas.height - h, barWidth, h);
x += barWidth + 1;
}
}
document.querySelectorAll('.pad').forEach(pad => {
pad.addEventListener('click', () => {
initAudio();
playNote(notesFreq[pad.dataset.note]);
pad.classList.add('active');
setTimeout(() => pad.classList.remove('active'), 150);
});
});
function createSequencer() {
noteKeys.forEach((note, rowIndex) => {
const row = document.createElement('div');
row.className = 'sequencer-row';
for (let i = 0; i < stepsCount; i++) {
const stepBtn = document.createElement('div');
stepBtn.className = 'step';
stepBtn.onclick = () => {
stepBtn.classList.toggle('active');
sequencerData[rowIndex][i] = stepBtn.classList.contains('active');
};
row.appendChild(stepBtn);
}
sequencerDiv.appendChild(row);
});
}
function playStep() {
if (!playing) return;
const interval = (60 / tempoInput.value) / 4 * 1000;
document.querySelectorAll('.step.playing').forEach(el => el.classList.remove('playing'));
noteKeys.forEach((note, rowIndex) => {
const stepEl = sequencerDiv.children[rowIndex].children[currentStep];
stepEl.classList.add('playing');
if (sequencerData[rowIndex][currentStep]) playNote(notesFreq[note]);
});
currentStep = (currentStep + 1) % stepsCount;
nextStepTimeout = setTimeout(playStep, interval);
}
startBtn.addEventListener('click', () => {
initAudio();
playing = true;
startBtn.disabled = true;
stopBtn.disabled = false;
currentStep = 0;
playStep();
});
stopBtn.addEventListener('click', () => {
playing = false;
clearTimeout(nextStepTimeout);
startBtn.disabled = false;
stopBtn.disabled = true;
document.querySelectorAll('.step.playing').forEach(el => el.classList.remove('playing'));
});
tempoInput.addEventListener('input', () => tempoValue.textContent = tempoInput.value);
createSequencer();
</script>
</body>
</html>
Мини-гольф с препятствиями
Описание игры
Это простая и увлекательная мини-гольф игра, где игрок управляет шариком на игровом поле с препятствиями. Задача – попасть шаром в лунку за минимальное количество ходов. Направление и сила удара задаются касанием и перемещением указателя мыши или пальца по экрану. После попадания выводится поздравление и игра сбрасывается. Есть кнопка для ручного сброса.
Возможности игры
- Управление: тяни шарик в нужном направлении, чтобы задать скорость и направление удара.
- Физика движения: шарик двигается по полю с имитацией трения и отражается от стен и препятствий.
- Препятствия: два вертикальных блока, от которых шарик отскакивает с потерей скорости.
- Лунка: цель, попадание в которую отслеживается и сопровождается подсветкой и сообщением.
- Подсчёт ходов: отображение числа ударов, увеличивающегося при каждом броске.
- Сброс игры: кнопка позволяет начать заново с исходной позиции и нуля ходов.
- Адаптивный холст: размер игрового поля автоматически подстраивается под ширину экрана с сохранением пропорций.
- Поддержка мыши и тач-событий: управление доступно как на ПК, так и на мобильных устройствах.
- Холст размером 600x400 пикселей с автоматическим масштабированием по ширине экрана.
- Событие
resizeокна вызывает функцию, пересчитывающую размеры канваса для адаптивности. - На странице отображается счётчик ходов и кнопка сброса игры.
- Шарик: с координатами, радиусом, скоростью (vx, vy) и коэффициентом трения (friction = 0.97).
- Лунка: с координатами, радиусом, визуальной подсветкой при попадании и таймером подсветки.
- Препятствия: два прямоугольных блока с фиксированными размерами и позициями.
- Функции
drawBall(),drawHole(),drawObstacles()рисуют соответственно шарик, лунку и препятствия на канвасе, с учётом визуальных эффектов подсветки и теней. - Функция
drawAimLine()рисует линию от шара до текущей точки захвата мыши/пальца при перетягивании для удобства прицеливания. - В функции
update()реализована основная логика движения шарика: - при отсутствии перетягивания добавляются небольшие случайные шумы к скорости для реалистичности.
- скорость и позиция шарика обновляются с учётом трения.
- проверяется столкновение с границами поля, с отскоком и потере скорости.
- вызывается
checkObstacleCollision()для обработки столкновений с препятствиями: учитывается ближайшая точка на прямоугольнике, и при контакте изменяется скорость с коэффициентом отражения -0.7. - если скорости очень малы, они обнуляются (остановка).
- проверяется попадание в лунку, при котором активируется подсветка, считается ход, выводится alert с поздравлением, и начинается отсчёт до сброса уровня.
- Обработчики событий мыши и тача:
mousedown/touchstartпроверяют попадание в шарик и начинают перетягивание (isDragging = true).mousemove/touchmoveобновляют текущую точку захвата для визуальной линии.mouseup/touchendвычисляют вектор силы от точки отпускания к шару, ограничивают максимальную скорость, устанавливают скорость шара, завершают перетягивание, увеличивают счёт ходов и обновляют отображение.- Обработчик
mouseleaveотменяет перетягивание, если указатель покидает холст. - Функция
resetGame()возвращает шарик в исходную позицию, сбрасывает скорость, обнуляет счёт ходов и подсветку лунки. - Кнопка "Сбросить игру" вызывает эту функцию.
- Функция
loop()вызывается черезrequestAnimationFrame, обновляя физику и рисуя кадры. - Запуск игры происходит сразу после инициализации.
- Используются плавные анимации взаимодействия, благодаря регуляции трения и постепенной остановке шарика.
- Реализация отскока реализована через отражение скорости с уменьшением энергии, что придаёт естественность движению.
- Визуальная подсветка лунки при попадании и задержка перед сбросом создаёт приятный пользовательский опыт.
- Поддержка и мыши, и сенсорных устройств обеспечивает широкую совместимость.
- Интерфейс и визуальные эффекты выдержаны в минималистичном и современном стиле, гармонично сочетаясь с общей тёмной темой.
Подробный разбор кода
1. Настройка канваса и адаптивность
- Канвас создаётся с базовыми размерами 600x400 пикселей.
- Функция
resizeCanvas()подгоняет визуальный размер канваса под ширину окна, сохраняя пропорции, вызывается при загрузке страницы и при изменении размера окна. - Объекты
ball(шарик) иhole(лунка) хранят позиции, размеры, скорости (только у шарика), а также состояние подсветки у лунки. - Массив
obstaclesсодержит прямоугольные препятствия с координатами и размерами. - Переменная
movesотслеживает количество ходов, обновляется в DOM. - Флаги
isDraggingиdragStartотвечают за состояние взаимодействия пользователя с шариком. - Функции
drawBall(),drawHole()иdrawObstacles()рисуют на канвасе объекты с учетом теней и подсветки. drawAimLine()показывает линию от центра шарика к текущей точке захвата, если игрок тянет шарик.- Функция
update()обновляет положение шарика: - При отсутствии перетягивания к скорости добавляется небольшой шум для естественности.
- Позиции меняются согласно скоростям, последние уменьшаются трением (0.97).
- Реализованы отражения шарика от границ поля с уменьшением скорости на каждом столкновении.
- Вызов
checkObstacleCollision()проверяет попадание в препятствия и изменяет скорость с коэффициентом отражения -0.7 с коррекцией позиции. - При очень малых скоростях скорость сбрасывается, чтобы шарик останавливался.
- Проверяется попадание в лунку; если шар попал – включается подсветка, выводится alert с результатом и запускается таймер подсветки.
- При нажатии (
mousedown/touchstart) проверяется, попал ли курсор/палец на шарик с запасом 10 пикселей; если да, включается режим перетягивания. - Во время движения (
mousemove/touchmove) обновляется точка захвата – для отображения линии прицеливания. - При отпускании (
mouseup/touchend) считается сила удара как вектор от точки отпускания к шару, ограничивается максимальная скорость, присваивается скорость объекту шарика. Ход засчитывается, переключается режим перетягивания. - При уходе курсора с поля (
mouseleave) режим перетягивания отменяется. - Функция
resetGame()возвращает шарик на стартовую позицию, обнуляет скорость и счёт ходов, сбрасывает состояние подсветки. - Кнопка «Сбросить игру» вызывает
resetGame()для удобства игрока. - Функция
loop()вызывается циклично черезrequestAnimationFrame: обновляет состояние физики и перерисовывает весь кадр. - Запуск цикла происходит сразу при загрузке игры.
Полный код игры:
game4.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Мини-гольф с препятствиями</title>
<style>
html, body {
margin: 0; height: 100%;
background: #121212;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #eee;
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 100vh;
padding: 10px 5px 15px;
}
#rules {
background: #222;
padding: 12px 16px;
border-radius: 12px;
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
font-size: 1rem;
line-height: 1.4;
box-shadow: 0 0 15px rgba(100, 149, 237, 0.4);
}
#rules strong { color: #90caf9; }
#info { font-size: 1.1rem; margin-bottom: 10px; text-align: center; font-weight: bold; }
button {
margin-bottom: 10px; padding: 12px 30px; border-radius: 12px; border: none;
background-color: #ff6f61; color: white; font-weight: 600; cursor: pointer;
box-shadow: 0 4px 0 #b34a41; transition: transform 0.1s;
font-size: 1.1rem; max-width: 600px; width: 100%; box-sizing: border-box;
}
button:active { transform: translateY(3px); box-shadow: none; }
canvas {
background: #1a1a1a;
border-radius: 12px;
box-shadow: 0 0 30px #000;
max-width: 600px;
width: 100%;
height: auto;
touch-action: none;
}
</style>
</head>
<body>
<div id="rules">
<strong>Правила мини-гольфа:</strong>
<ul style="margin: 5px 0; padding-left: 20px;">
<li>Тяни от шарика, чтобы прицелиться и задать силу.</li>
<li>Цвет линии подскажет мощность удара.</li>
<li>Цель — лунка. Чем меньше ходов, тем лучше!</li>
</ul>
</div>
<div id="info">Ходов: <span id="moves">0</span></div>
<button id="resetBtn">Сбросить игру</button>
<canvas id="game" width="600" height="400"></canvas>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const movesSpan = document.getElementById('moves');
const resetBtn = document.getElementById('resetBtn');
const BASE_WIDTH = 600;
const BASE_HEIGHT = 400;
const ball = { x: 100, y: 200, radius: 10, vx: 0, vy: 0, friction: 0.98 };
const hole = { x: 520, y: 200, radius: 15 };
const obstacles = [
{ x: 250, y: 100, width: 20, height: 200 },
{ x: 400, y: 50, width: 20, height: 150 }
];
let moves = 0;
let isDragging = false;
let dragStart = null;
let winAlerted = false;
function draw() {
ctx.clearRect(0, 0, BASE_WIDTH, BASE_HEIGHT);
// Лунка
ctx.beginPath();
ctx.fillStyle = '#000';
ctx.strokeStyle = '#90caf9';
ctx.lineWidth = 3;
ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Препятствия
ctx.fillStyle = '#555a85';
ctx.shadowBlur = 10;
ctx.shadowColor = '#444';
obstacles.forEach(obs => {
ctx.fillRect(obs.x, obs.y, obs.width, obs.height);
ctx.strokeStyle = '#666eb2';
ctx.strokeRect(obs.x, obs.y, obs.width, obs.height);
});
ctx.shadowBlur = 0;
// Линия прицела
if (isDragging && dragStart) {
const dx = ball.x - dragStart.x;
const dy = ball.y - dragStart.y;
const dist = Math.hypot(dx, dy);
const intensity = Math.min(dist / 100, 1);
ctx.beginPath();
ctx.moveTo(ball.x, ball.y);
ctx.lineTo(ball.x + dx, ball.y + dy);
ctx.strokeStyle = `rgb(${intensity * 255}, ${255 - intensity * 255}, 255)`;
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
}
// Шарик
ctx.beginPath();
ctx.fillStyle = '#ff6f61';
ctx.shadowBlur = 15;
ctx.shadowColor = '#ff6f61';
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
function update() {
if (isDragging) return;
ball.x += ball.vx;
ball.y += ball.vy;
ball.vx *= ball.friction;
ball.vy *= ball.friction;
// Стены
if (ball.x < ball.radius || ball.x > BASE_WIDTH - ball.radius) {
ball.vx *= -0.7;
ball.x = ball.x < ball.radius ? ball.radius : BASE_WIDTH - ball.radius;
}
if (ball.y < ball.radius || ball.y > BASE_HEIGHT - ball.radius) {
ball.vy *= -0.7;
ball.y = ball.y < ball.radius ? ball.radius : BASE_HEIGHT - ball.radius;
}
// Столкновения с препятствиями
obstacles.forEach(obs => {
let cx = Math.max(obs.x, Math.min(ball.x, obs.x + obs.width));
let cy = Math.max(obs.y, Math.min(ball.y, obs.y + obs.height));
let dist = Math.hypot(ball.x - cx, ball.y - cy);
if (dist < ball.radius) {
if (Math.abs(ball.x - cx) > Math.abs(ball.y - cy)) ball.vx *= -0.7;
else ball.vy *= -0.7;
// Выталкивание
let angle = Math.atan2(ball.y - cy, ball.x - cx);
ball.x = cx + Math.cos(angle) * ball.radius;
ball.y = cy + Math.sin(angle) * ball.radius;
}
});
// Попадание в лунку
let distToHole = Math.hypot(ball.x - hole.x, ball.y - hole.y);
if (distToHole < hole.radius) {
// Эффект засасывания
ball.vx *= 0.5; ball.vy *= 0.5;
if (distToHole < 5 && !winAlerted) {
winAlerted = true;
setTimeout(() => {
alert(`В лунке! Ходов: ${moves}`);
resetGame();
}, 100);
}
}
if (Math.abs(ball.vx) < 0.1) ball.vx = 0;
if (Math.abs(ball.vy) < 0.1) ball.vy = 0;
}
function resetGame() {
ball.x = 100; ball.y = 200; ball.vx = 0; ball.vy = 0;
moves = 0; movesSpan.textContent = "0";
winAlerted = false;
}
function getMousePos(e) {
const rect = canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: (clientX - rect.left) * (BASE_WIDTH / rect.width),
y: (clientY - rect.top) * (BASE_HEIGHT / rect.height)
};
}
const startDrag = (e) => {
const pos = getMousePos(e);
if (Math.hypot(pos.x - ball.x, pos.y - ball.y) < ball.radius * 3) {
isDragging = true;
dragStart = pos;
}
};
const doDrag = (e) => { if (isDragging) dragStart = getMousePos(e); };
const endDrag = (e) => {
if (isDragging) {
const pos = getMousePos(e.changedTouches ? e.changedTouches[0] : e);
ball.vx = (ball.x - pos.x) * 0.12;
ball.vy = (ball.y - pos.y) * 0.12;
isDragging = false;
moves++;
movesSpan.textContent = moves;
}
};
canvas.addEventListener('mousedown', startDrag);
window.addEventListener('mousemove', doDrag);
window.addEventListener('mouseup', endDrag);
canvas.addEventListener('touchstart', (e) => { e.preventDefault(); startDrag(e); }, {passive:false});
window.addEventListener('touchmove', (e) => { if(isDragging) e.preventDefault(); doDrag(e); }, {passive:false});
window.addEventListener('touchend', endDrag);
resetBtn.onclick = resetGame;
function loop() { update(); draw(); requestAnimationFrame(loop); }
loop();
})();
</script>
</body>
</html>
Три в ряд – квадратная сетка
Описание игры
Это классическая игра «три в ряд», выполненная на квадратной сетке 8x8. Игрок меняет местами соседние шарики разного цвета, чтобы сформировать горизонтальные или вертикальные линии из трёх и более одинаковых по цвету фигур. Для разбивания специальных блоков нужно подбирать правильные ходы. Игра поддерживает два режима: по ходам и по времени, а также три уровня сложности, влияющие на палитру цветов и вероятность появления блоков.
Возможности игры
- Размер поля: 8×8 клеток с шариками.
- Цвета: от 6 до 10 цветов в зависимости от уровня сложности.
- Специальные блоки: блоки с прочностью, которые нужно разбить.
- Режимы игры: по ходам (лимит ходов) и по времени (таймер).
- Уровни сложности: Лёгкий, Средний, Сложный, влияющие на цвета, количество ходов и таймер.
- Перетаскивание: поддержка drag-and-drop мышью и касаниями.
- Комбо: последовательность совпадений увеличивает множитель очков.
- Анимации: плавное исчезновение шариков, подсвечивание перетаскиваемых элементов.
- Доступность: aria-метки и семантика для удобства пользователей.
- Определены уровни со свойствами: цвета, количество ходов, шанс блоков и время.
- Создаётся сетка 8×8, генерируются клетки с шариками случайных цветов из палитры выбранного уровня.
- Блоки появляются с определённой вероятностью и имеют прочность (2 удара).
- Элементы созданы динамически с классами, отражающими состояние (цвета, блок, пустая клетка).
- Реализован drag-and-drop через мышь и поддержку touch-событий на мобильных устройствах.
- Проверяется соседство меняемых клеток – обмен разрешён только между соседними по вертикали или горизонтали.
- После перемещения производится проверка совпадений.
- Проверяются горизонтальные и вертикальные линии из 3 и более одинаковых шариков.
- Совпадающие позиции собираются в множество, которое затем обрабатывается:
- шарики анимировано исчезают (
removing), после чего становятся пустыми. - за совпадения начисляются очки с учётом множителя комбо.
- если совпадение рядом с блоками, последние «бьются» и меняют вид при снижении прочности; после разрушения блок превращается в обычный шарик.
- После очистки клетки сверху «падают» вниз по столбцу, и сверху появляются новые шарики.
- Повторная проверка совпадений осуществляется рекурсивно до отсутствия новых линий.
- Поддерживаются переменные
score,combo,movesLeft,timeLeftдля отображения и логики. - Комбо растёт при цепочке совпадений и обнуляется при отсутствии совпадений.
- При достижении нуля ходов или времени показывается итоговое сообщение и игра сбрасывается.
- Элементы управления – кнопки выбора уровня и режима, счёт, оставшиеся ходы или время, отображение текущего комбо.
- Все кнопки снабжены aria-атрибутами для доступности.
- Дизайн выдержан в тёмных тонах с контрастными цветами.
- Для режима с таймером запускается интервал, который каждую секунду уменьшает время, обновляет счётчик и завершает игру по окончании времени.
- При смене режима или уровня игра сбрасывается и стартует заново.
Подробный разбор кода
1. Инициализация и создание игрового поля
- Объявлены уровни с параметрами: палитра цветов, количество ходов, шанс блоков и время для таймера.
- Определены размеры поля: 8 колонок и 8 рядов.
- Функция
createBoard()создаёт 64 клетки (div.cell) и кладёт в каждую шарик (div.ball) с цветом из палитры или с блоком с прочностью. - Блоки имеют классы
blockиhit1илиhit2, а шарики блоков становятся неактивными для перетаскивания. updateScore(),updateCounter(),updateCombo()обновляют текстовые элементы страницы с текущими значениями.- Функция
areNeighbors(id1, id2)проверяет, соседствуют ли две клетки по горизонтали или вертикали. getColor(elem)возвращает цвет шарика в клетке.swapSquares(id1, id2)меняет классы цветов шариков двух клеток, если это разрешено (без блоков, без анимации удаления).checkMatches()ищет горизонтальные и вертикальные линии из 3+ одинаковых цветов.- При совпадениях:
- Включается
bonusMode, увеличиваетсяcombo, показывается текст комбо. - Вызывается
removeSquare(pos)для анимации исчезновения шариков. - Бонусные очки начисляются с множителем комбо.
- Рядом с удалёнными слотами снижается прочность блоков через
hitBlocksNear(pos). - После задержки происходит
collapseBoard(). collapseBoard()смещает шарики по колонкам вниз, заполняя пустые клетки цветными новыми шариками из палитры.- После обновления проверяется, есть ли новые совпадения, иначе
comboсбрасывается. - Реализованы обработчики drag-and-drop для мыши:
dragStart,dragEnd,dragOver,dragDrop. - Выбор клетки для перетаскивания, проверка соседства и попытка обмена. Если совпадения нет, меняется местами обратно.
- Поддержка сенсорного ввода:
touchStart,touchMove,touchEndс вычислением смещения и определением ближайшей соседней клетки. resetGame()инициализирует начальное состояние, создаёт поле, сбрасывает счёт и комбо, запускает таймер для режима по времени.- Кнопки уровней и режимов меняют параметры и перезапускают игру.
- Таймер (
setInterval) по времени уменьшаетtimeLeftи обновляет отображение, заканчивает игру при достижении 0.
Полный код игры:
game5.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Три в ряд </title>
<style>
body {
margin: 0; padding: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #554e73;
color: #fff;
user-select: none;
display: flex; flex-direction: column; align-items: center;
min-height: 100vh;
}
#game-container {
margin-top: 20px;
width: 100%;
max-width: 480px;
padding: 0 10px;
box-sizing: border-box;
}
#rules {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 10px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.4;
}
#controls {
margin-bottom: 12px;
display: flex; justify-content: center; flex-wrap: wrap; gap: 10px;
}
button.level, button.mode {
background: #444; border: none; border-radius: 8px; color: #eee;
font-weight: 700; padding: 10px 20px; cursor: pointer;
transition: background 0.3s; min-width: 90px; font-size: 16px;
}
button.level.active, button.mode.active { background: #90caf9; color: #121212; }
#score, #counter, #combo {
font-size: 20px; font-weight: 700; margin: 6px 0;
text-shadow: 0 0 5px #000; text-align: center;
}
#game {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
gap: 6px;
background: #554e73;
border-radius: 12px;
touch-action: none;
aspect-ratio: 1/1;
}
.cell {
position: relative; width: 100%; aspect-ratio: 1/1;
border-radius: 50%;
}
.ball {
position: absolute; inset: 0; border-radius: 50%;
background: radial-gradient(circle at 30% 35%, rgba(255,255,255,0.3), transparent 60%);
box-shadow: inset 0 2px 6px rgba(255,255,255,0.5), inset 0 -5px 10px rgba(0,0,0,0.6);
cursor: grab; transition: transform 0.2s, opacity 0.3s;
}
.ball.dragging { transform: scale(1.2); z-index: 10; cursor: grabbing; }
.red { background-color: #8b2c2c; }
.green { background-color: #317a31; }
.blue { background-color: #1c5fa3; }
.yellow { background-color: #bfae1e; }
.orange { background-color: #b36b18; }
.purple { background-color: #613a7f; }
.cyan { background-color: #1b7d87; }
.pink { background-color: #8c3360; }
.lime { background-color: #8a8f2a; }
.block .ball { background: #555 !important; border-radius: 8px; box-shadow: inset 0 0 12px #222 !important; cursor: not-allowed !important; }
.block.hit1 .ball { background: #777 !important; border: 1px dashed #fff; }
.removing { animation: fadeOutScale 0.4s forwards; }
@keyframes fadeOutScale {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0.3); }
}
</style>
</head>
<body>
<div id="game-container">
<div id="rules">
<strong>Правила:</strong> Собирай линии от 3 шаров. Блоки разбиваются рядом с комбинациями. Выбери режим и сложность!
</div>
<div id="controls">
<button class="level active" data-level="easy">Лёгкий</button>
<button class="level" data-level="medium">Средний</button>
<button class="level" data-level="hard">Сложный</button>
<button class="mode active" data-mode="moves">По ходам</button>
<button class="mode" data-mode="time">По времени</button>
</div>
<div id="score">Счёт: 0</div>
<div id="counter">Ходов осталось: 30</div>
<div id="game"></div>
</div>
<script>
(() => {
const levels = {
easy: { colors: 6, moves: 30, blockChance: 0.07, time: 60 },
medium: { colors: 8, moves: 25, blockChance: 0.10, time: 45 },
hard: { colors: 9, moves: 20, blockChance: 0.14, time: 30 }
};
const colorPool = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'cyan', 'pink', 'lime'];
let currentLvl = 'easy';
let mode = 'moves';
let score = 0;
let moves = 30;
let timeLeft = 60;
let timerId = null;
let squares = [];
let isProcessing = false;
const game = document.getElementById('game');
const scoreDisplay = document.getElementById('score');
const counterDisplay = document.getElementById('counter');
function init() {
clearInterval(timerId);
game.innerHTML = '';
squares = [];
score = 0;
scoreDisplay.innerText = `Счёт: ${score}`;
const cfg = levels[currentLvl];
moves = cfg.moves;
timeLeft = cfg.time;
if(mode === 'moves') {
counterDisplay.innerText = `Ходов осталось: ${moves}`;
} else {
counterDisplay.innerText = `Время осталось: ${timeLeft}s`;
startTimer();
}
for(let i=0; i<64; i++) {
const sq = document.createElement('div');
sq.className = 'cell';
const ball = document.createElement('div');
ball.className = 'ball';
if(Math.random() < cfg.blockChance) {
sq.classList.add('block', 'hit2');
sq.dataset.hits = 2;
} else {
ball.classList.add(colorPool[Math.floor(Math.random() * cfg.colors)]);
}
sq.appendChild(ball);
game.appendChild(sq);
squares.push(sq);
}
checkMatches(true);
}
function getBallColor(sq) {
if(!sq || sq.classList.contains('block')) return null;
return colorPool.find(c => sq.querySelector('.ball').classList.contains(c));
}
function checkMatches(silent = false) {
let matched = new Set();
for(let r=0; r<8; r++) {
for(let c=0; c<6; c++) {
let i = r*8+c;
let c1 = getBallColor(squares[i]), c2 = getBallColor(squares[i+1]), c3 = getBallColor(squares[i+2]);
if(c1 && c1 === c2 && c1 === c3) { matched.add(i); matched.add(i+1); matched.add(i+2); }
}
}
for(let c=0; c<8; c++) {
for(let r=0; r<6; r++) {
let i = r*8+c;
let c1 = getBallColor(squares[i]), c2 = getBallColor(squares[i+8]), c3 = getBallColor(squares[i+16]);
if(c1 && c1 === c2 && c1 === c3) { matched.add(i); matched.add(i+8); matched.add(i+16); }
}
}
if(matched.size > 0) {
if(!silent) {
isProcessing = true;
matched.forEach(i => {
squares[i].querySelector('.ball').classList.add('removing');
hitBlocks(i);
});
score += matched.size * 10;
scoreDisplay.innerText = `Счёт: ${score}`;
setTimeout(() => {
matched.forEach(i => {
const b = squares[i].querySelector('.ball');
b.className = 'ball ' + colorPool[Math.floor(Math.random() * levels[currentLvl].colors)];
});
isProcessing = false;
checkMatches();
}, 400);
} else {
matched.forEach(i => {
const b = squares[i].querySelector('.ball');
b.className = 'ball ' + colorPool[Math.floor(Math.random() * levels[currentLvl].colors)];
});
checkMatches(true);
}
return true;
}
return false;
}
function hitBlocks(idx) {
[idx-1, idx+1, idx-8, idx+8].forEach(n => {
if(squares[n] && squares[n].classList.contains('block')) {
let h = parseInt(squares[n].dataset.hits);
if(h > 1) { squares[n].dataset.hits = 1; squares[n].classList.replace('hit2', 'hit1'); }
else {
squares[n].classList.remove('block', 'hit1');
squares[n].querySelector('.ball').className = 'ball ' + colorPool[0];
}
}
});
}
let dragSrc = null;
game.addEventListener('mousedown', e => {
const ball = e.target.closest('.ball');
if(!ball || isProcessing || ball.parentElement.classList.contains('block')) return;
dragSrc = ball.parentElement;
ball.classList.add('dragging');
});
window.addEventListener('mouseup', e => {
if(!dragSrc) return;
dragSrc.querySelector('.ball').classList.remove('dragging');
const target = e.target.closest('.cell');
if(target && target !== dragSrc && !target.classList.contains('block')) {
const i1 = squares.indexOf(dragSrc), i2 = squares.indexOf(target);
if([1, -1, 8, -8].includes(i1 - i2)) {
swap(dragSrc, target);
if(!checkMatches()) { setTimeout(() => swap(dragSrc, target), 200); }
else if(mode === 'moves') {
moves--; counterDisplay.innerText = `Ходов осталось: ${moves}`;
if(moves <= 0) { alert('Финиш! Счёт: ' + score); init(); }
}
}
}
dragSrc = null;
});
function swap(s1, s2) {
const b1 = s1.querySelector('.ball'), b2 = s2.querySelector('.ball');
const c1 = b1.className, c2 = b2.className;
b1.className = c2; b2.className = c1;
}
function startTimer() {
timerId = setInterval(() => {
timeLeft--;
counterDisplay.innerText = `Время осталось: ${timeLeft}s`;
if(timeLeft <= 0) { clearInterval(timerId); alert('Время вышло! Счёт: ' + score); init(); }
}, 1000);
}
// Обработчики кнопок
document.querySelectorAll('.level').forEach(b => b.onclick = () => {
document.querySelector('.level.active').classList.remove('active');
b.classList.add('active'); currentLvl = b.dataset.level; init();
});
document.querySelectorAll('.mode').forEach(b => b.onclick = () => {
document.querySelector('.mode.active').classList.remove('active');
b.classList.add('active'); mode = b.dataset.mode; init();
});
init();
})();
</script>
</body>
</html>
Тетрис
Описание игры
Тетрис – классическая и увлекательная игра-головоломка, где игрок управляет падающими фигурами разных форм и цветов на сетке размером 10×20 клеток. Цель – формировать полные горизонтальные линии из блоков, чтобы они исчезали, освобождая место для новых фигур и принося очки. Управление реализовано с помощью клавиатуры (стрелки и пробел) и сенсорных свайпов на мобильных устройствах. Игра предлагает выбор уровня сложности, отображает текущий счёт и уровень, а также обладает современным стильным интерфейсом с адаптивной графикой и плавной анимацией.
Возможности игры
- Игровое поле 10×20 с чёткой пиксельной графикой, масштабируемой под плотность экрана (DPI).
- Разноцветные фигуры классических форм (I, J, L, O, S, T, Z).
- Фигуры можно перемещать влево/вправо, ускорять их падение и вращать (по часовой стрелке).
- Автоматическое удаление заполненных горизонтальных линий с начислением очков и ростом уровня.
- Увеличение уровня с ростом счёта, что ускоряет падение фигур (игра становится сложнее).
- Индикаторы текущего счёта и уровня отображаются на боковой панели.
- Выбор сложности с четырьмя предустановленными скоростями падения фигур.
- Кнопки для старта и рестарта игры, с изменением состояния активности.
- Поддержка управления мышью, клавиатурой и сенсорными экранами (свайпы).
- Блокировка нежелательных жестов браузера во время игры (например, pull-to-refresh на мобильных).
- Плавная анимация отрисовки игрового поля и фигур, а также отображение финального экрана при окончании игры.
- Адаптивный и доступный интерфейс с aria-метками.
- Инициализация DOM-элементов: canvas для игрового поля, элементы вывода счёта, уровня, выбора скорости, кнопки управления.
- Константы
COLSиROWSзадают размер поля (10×20),BLOCK_SIZE– размер одной клетки в пикселях. fixDpi()корректирует canvas под текущий DPI устройства для чёткой отрисовки.- Функция
createBoard()создаёт внутренний массивboardс заполнением нулями (пустая сетка). - Массивы
SHAPESиCOLORSсодержат формы фигур (таблицы с координатами залитых блоков) и соответствующие им цвета. drawSquare(x, y, colorIndex)рисует отдельный блок с отступами и обводкой.drawBoard()рисует фон и заполненные клетки сетки.drawPiece()отрисовывает текущую падающую фигуру поверх поля.- Проверка столкновений с границами и другими блоками – функция
collide(offsetX, offsetY, shape). - Закрепление фигуры на поле –
mergePiece(). - Преобразование массива формы для поворота –
rotate(matrix). rotatePiece()пытается повернуть фигуру с учётом возможных смещений для предотвращения конфликтов с границами и занятыми ячейками.clearLines()ищет заполненные линии и удаляет их – сдвигает всё сверху вниз и добавляет пустые строки сверху.- Начисляет очки за количество удалённых линий, повышает уровень (до 10) и уменьшает интервал падения фигур.
updateDisplay()обновляет видимое значение счёта и уровня.- Функция
update(time)вызывается черезrequestAnimationFrame, которая отслеживает время, считает интервал и отвечает за падение фигуры. - При окончании игры рисуется затемнение с сообщением «Игра окончена», итоговым счётом и подсказкой начать заново.
- Функция
dropPiece()пытается сместить фигуру вниз; если столкновение – закрепляет фигуру и создаёт новую. - Обработка клавиатурных событий:
- Стрелки влево/вправо – перемещение фигуры.
- Стрелка вниз – ускоренное падение.
- Стрелка вверх и пробел – вращение фигуры.
- Сенсорное управление:
- Свайпы влево/вправо – сдвиг фигуры.
- Свайп вниз – ускоренный сброс фигуры.
- Свайп вверх или тап – вращение фигуры.
- Блокировка pull-to-refresh на мобильных устройствах при свайпах вниз на канвасе с помощью предотвращения события по условию.
- Кнопка
Стартзапускает игру, сбрасывая состояние, устанавливая интервалы и активируя цикл рисования. - Кнопка
Начать зановоперезапускает игру, становится активной только во время игры. - Состояние игры контролируется переменными
running,gameOver,fastDropи др.
Подробный разбор кода
1. Инициализация и подготовка поля
- Получение элементов DOM: канвас, счёт, уровень, выбор скорости, кнопки старт и рестарт.
- Константы задают размеры поля – 10 столбцов, 20 рядов, размер блока 24 пикселя.
- Массив
boardсоздаётся приcreateBoard()– двухмерный массив, заполненный нулями. - Функция
fixDpi()масштабирует канвас под устройство с учётом плотности пикселей (devicePixelRatio). drawSquare(x, y, colorIndex)рисует отдельный блок с цветом и обводкой.drawBoard()заполняет фон и отрисовывает все уже закреплённые фигуры на поле.drawPiece()рисует падающую фигуру согласно её форме и координатам.collide(offsetX, offsetY, shape)проверяет столкновения фигуры с краями и другими блоками.mergePiece()фиксирует фигуру на поле, копируя ее в массивboard.- Функция
rotate(matrix)реализует поворот фигуры на 90° по часовой стрелке, используя транспозицию и инверсию матрицы. rotatePiece()пытается повернуть фигуру с учётом сдвигов по горизонтали, чтобы избежать столкновений.clearLines()проходит по рядам снизу вверх, удаляет заполненные линии, добавляет пустые сверху.- Начисляет очки за количество удалённых линий умноженных на текущий уровень.
- Увеличивает уровень и соответственно ускоряет падение фигур, корректируя интервал
dropInterval. update(time)вызывается черезrequestAnimationFrame: управляет временем падения фигур.- Если время набрано, вызывается
dropPiece(), фрагмент кода сдвигает фигуру вниз или фиксирует в поле. - При окончании игры отрисовывает затемнение и сообщение с подсказкой начать заново.
- Обработчик
keydownреагирует на стрелки и пробел: движение влево/вправо, падение вниз, вращение. - Сенсорные события
touchstart,touchmove,touchendреализуют управление свайпами: - Свайпы влево/вправо сдвигают фигуру.
- Свайп вниз ускоряет падение.
- Свайп вверх или лёгкое касание вызывает вращение.
- Для предотвращения нежелательного scroll pull-to-refresh при свайпе вниз на канвасе активна блокировка touchmove с
e.preventDefault. - Кнопка
Стартзапускает игру, инициирует переменные, создаёт игровое поле и новую фигуру. - Кнопка
Начать зановоперезапускает игру с текущими настройками. - Переменные
running,gameOver,fastDropконтролируют состояние – запущена ли игра, окончена ли, ускоренное ли падение. - Таймеры управления быстрой отрисовкой падения и сброса ускоренного падения реализованы через
setTimeout.
Полный код игры:
game6.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Тетрис</title>
<style>
body {
margin: 0;
background: linear-gradient(135deg, #0d1117, #161b22);
color: #e6e8ea;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
user-select: none;
}
h1 {
margin: 12px 0 24px;
font-weight: 800;
color: #58a6ff;
text-shadow: 0 0 8px #58a6ff88;
font-size: 2.6rem;
}
#container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 24px;
max-width: 480px;
width: 100%;
}
#game {
background: #0f161f;
border-radius: 16px;
box-shadow: 0 0 40px #2389d0cc inset, 0 8px 30px #145e96cc;
touch-action: pan-y;
outline: none;
cursor: default;
transition: box-shadow 0.3s ease;
width: 240px;
height: 480px;
display: block;
margin: 0 auto;
image-rendering: pixelated;
}
#game:focus {
box-shadow:
0 0 48px #58a6ffcc inset,
0 0 40px #58a6ff88,
0 10px 38px #58a6ffcc;
}
#sidebar {
width: 240px;
display: flex;
flex-direction: column;
gap: 20px;
background: linear-gradient(145deg, #1c2633, #121a26);
padding: 24px;
border-radius: 20px;
box-shadow: 0 12px 36px rgba(10,30,60,0.7);
user-select: none;
}
#score, #level {
font-size: 1.4rem;
font-weight: 800;
padding: 14px 20px;
background: linear-gradient(90deg, #204060, #1e3a66);
border-radius: 18px;
text-align: center;
color: #a3c4ff;
text-shadow: 0 0 10px #3f70cc;
box-shadow: 0 2px 12px #2468c6aa;
}
#rules {
max-width: 100%;
background: #1a2332;
padding: 18px 20px;
border-radius: 16px;
font-size: 0.9rem;
line-height: 1.5;
color: #bec9e2cc;
box-shadow: inset 0 0 8px #0a1227;
user-select: none;
overflow-y: auto;
max-height: 180px;
font-weight: 500;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#rules ul {
padding-left: 20px;
margin: 8px 0 0 0;
}
#rules li {
margin-bottom: 10px;
}
button {
padding: 14px 0;
font-size: 1.2rem;
font-weight: 900;
border-radius: 20px;
background: linear-gradient(135deg, #409cff, #0066ff);
box-shadow:
0 0 24px #3f88ffcc,
inset 0 -3px 6px #0e5890cc;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s ease, box-shadow 0.3s ease;
user-select: none;
}
button:hover, button:focus {
background: linear-gradient(135deg, #0053cc, #0041a8);
box-shadow:
0 0 30px #0053ccdd,
inset 0 -3px 12px #002a65cc;
outline: none;
}
label {
font-size: 1.1rem;
user-select: none;
display: flex;
align-items: center;
gap: 14px;
font-weight: 600;
color: #aac2ffdd;
}
select {
flex-grow: 1;
background: #223250;
color: #add0ff;
border-radius: 14px;
border: none;
padding: 8px 14px;
font-size: 1.05rem;
cursor: pointer;
box-shadow: inset 0 0 12px rgba(0,0,0,0.7);
transition: background 0.3s, color 0.3s;
font-weight: 600;
}
select:hover, select:focus {
background: #2b3a66;
color: #d6e4ff;
outline: none;
}
@media (max-width: 650px) {
#container {
max-width: 100%;
flex-direction: column;
align-items: center;
}
#sidebar {
width: 100%;
padding: 18px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 18px;
}
#rules {
max-height: 120px;
flex-basis: 100%;
order: 3;
}
#score, #level {
flex-grow: 1;
font-size: 1.2rem;
}
button {
flex-grow: 2;
padding: 12px;
font-size: 1.1rem;
order: 2;
}
label {
flex-grow: 2;
order: 1;
}
}
</style>
</head>
<body>
<h1>Тетрис</h1>
<div id="container">
<canvas id="game" width="240" height="480" aria-label="Игровое поле Тетриса" tabindex="0"></canvas>
<div id="sidebar">
<div id="score">Очки: 0</div>
<div id="level">Уровень: 1</div>
<label for="speedSelect">Сложность:
<select id="speedSelect" aria-label="Выбор уровня сложности">
<option value="800">Очень легко</option>
<option value="500" selected>Средне</option>
<option value="300">Сложно</option>
<option value="150">Очень сложно</option>
</select>
</label>
<button id="startBtn" aria-label="Начать игру">Старт</button>
<button id="restartBtn" aria-label="Начать заново" disabled>Начать заново</button>
<div id="rules" aria-label="Правила тетриса">
<strong>Правила игры:</strong>
<ul>
<li>Используй стрелки влево ← / вправо → для перемещения фигуры.</li>
<li>Стрелка вниз ↓ \u2014 ускорить падение фигуры.</li>
<li>Пробел или стрелка вверх ↑ \u2014 поворот фигуры.</li>
<li>Собирай горизонтальные линии, чтобы они исчезали и приносили очки.</li>
<li>Игра заканчивается, если фигуры достигают верха поля.</li>
</ul>
</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('score');
const levelDisplay = document.getElementById('level');
const speedSelect = document.getElementById('speedSelect');
const startBtn = document.getElementById('startBtn');
const restartBtn = document.getElementById('restartBtn');
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 24;
let board = [];
let score = 0;
let level = 1;
let dropInterval = 500;
let dropCounter = 0;
let lastTime = 0;
let gameOver = false;
let fastDrop = false;
let fastDropTimeout;
let running = false;
let piece = null;
let pieceX = 0;
let pieceY = 0;
const COLORS = [
null,
'#f44336',
'#2196f3',
'#ffeb3b',
'#9c27b0',
'#4caf50',
'#ff9800',
'#00bcd4',
];
const SHAPES = [
[],
[ // I
[0,0,0,0],
[1,1,1,1],
[0,0,0,0],
[0,0,0,0],
],
[ // J
[2,0,0],
[2,2,2],
[0,0,0],
],
[ // L
[0,0,3],
[3,3,3],
[0,0,0],
],
[ // O
[4,4],
[4,4],
],
[ // S
[0,5,5],
[5,5,0],
[0,0,0],
],
[ // T
[0,6,0],
[6,6,6],
[0,0,0],
],
[ // Z
[7,7,0],
[0,7,7],
[0,0,0],
],
];
function fixDpi() {
const dpi = window.devicePixelRatio || 1;
canvas.width = COLS * BLOCK_SIZE * dpi;
canvas.height = ROWS * BLOCK_SIZE * dpi;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpi, dpi);
}
fixDpi();
window.addEventListener('resize', () => {
fixDpi();
draw();
});
function createBoard() {
board = [];
for(let y=0; y<ROWS; y++) {
board[y] = new Array(COLS).fill(0);
}
}
function drawSquare(x, y, colorIndex) {
const xPx = Math.floor(x * BLOCK_SIZE);
const yPx = Math.floor(y * BLOCK_SIZE);
ctx.fillStyle = COLORS[colorIndex];
ctx.fillRect(xPx+1, yPx+1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
ctx.strokeStyle = "#101820";
ctx.lineWidth = 2;
ctx.strokeRect(xPx+1, yPx+1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
}
function drawBoard() {
ctx.fillStyle = '#1f1f1f';
ctx.fillRect(0, 0, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
for(let y=0; y<ROWS; y++) {
for(let x=0; x<COLS; x++) {
if(board[y][x] !== 0) {
drawSquare(x, y, board[y][x]);
}
}
}
}
function drawPiece() {
piece.shape.forEach((row, y) => {
row.forEach((value, x) => {
if(value !== 0) {
drawSquare(pieceX + x, pieceY + y, value);
}
});
});
}
function emptyTopRows(shape) {
let emptyRows = 0;
for(let y = 0; y < shape.length; y++) {
if(shape[y].every(cell => cell === 0)) emptyRows++;
else break;
}
return emptyRows;
}
function collide(offsetX=0, offsetY=0, shape = piece.shape) {
for(let y=0; y<shape.length; y++) {
for(let x=0; x<shape[y].length; x++) {
if(shape[y][x] !== 0) {
let newX = pieceX + x + offsetX;
let newY = pieceY + y + offsetY;
if(newX < 0 || newX >= COLS || newY >= ROWS) return true;
if(newY >= 0 && board[newY][newX] !== 0) return true;
}
}
}
return false;
}
function mergePiece() {
piece.shape.forEach((row, y) => {
row.forEach((value, x) => {
if(value !== 0 && pieceY + y >= 0) {
board[pieceY + y][pieceX + x] = value;
}
});
});
}
function rotate(matrix) {
const N = matrix.length;
const result = [];
for(let y = 0; y < N; y++) {
result[y] = [];
for(let x = 0; x < N; x++) {
result[y][x] = matrix[N - 1 - x][y];
}
}
return result;
}
function rotatePiece() {
const rotated = rotate(piece.shape);
if(!collide(0, 0, rotated)) {
piece.shape = rotated;
} else {
if(!collide(-1, 0, rotated)) {
piece.shape = rotated;
pieceX--;
} else if(!collide(1, 0, rotated)) {
piece.shape = rotated;
pieceX++;
}
}
}
function clearLines() {
let lines = 0;
outer: for(let y = ROWS -1; y >= 0; y--) {
for(let x = 0; x < COLS; x++) {
if(board[y][x] === 0) continue outer;
}
board.splice(y, 1);
board.unshift(new Array(COLS).fill(0));
lines++;
y++;
}
if(lines > 0) {
score += lines * 10 * level;
level = Math.min(10, Math.floor(score / 100) + 1);
dropInterval = Math.floor(parseInt(speedSelect.value) / level);
updateDisplay();
}
}
function updateDisplay() {
scoreDisplay.textContent = `Очки: ${score}`;
levelDisplay.textContent = `Уровень: ${level}`;
}
function newPiece() {
const id = Math.floor(Math.random() * (SHAPES.length - 1)) + 1;
piece = {
shape: SHAPES[id].map(row => row.slice()),
id
};
pieceX = Math.floor(COLS / 2) - Math.ceil(piece.shape[0].length / 2);
pieceY = -emptyTopRows(piece.shape);
if(collide(0, 0)) {
gameOver = true;
stopGame();
}
}
function resetGame() {
score = 0;
level = 1;
dropInterval = Math.floor(parseInt(speedSelect.value));
createBoard();
newPiece();
gameOver = false;
updateDisplay();
}
function draw() {
drawBoard();
if(piece) drawPiece();
}
function update(time = 0) {
if (!running) return;
if(gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
ctx.fillStyle = '#ff6f61';
ctx.font = 'bold 36px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Игра окончена', (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 - 10);
ctx.font = '20px sans-serif';
ctx.fillText(`Очки: ${score}`, (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 + 25);
ctx.fillText('Нажми "Начать заново"', (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 + 55);
stopGame();
return;
}
if(!lastTime) lastTime = time;
const delta = time - lastTime;
dropCounter += delta;
if(dropCounter > (fastDrop ? dropInterval / 5 : dropInterval)) {
dropPiece();
dropCounter = 0;
}
lastTime = time;
draw();
requestAnimationFrame(update);
}
function dropPiece() {
if(!collide(0, 1)) {
pieceY++;
} else {
mergePiece();
clearLines();
newPiece();
}
}
function startGame() {
if(running) return;
dropInterval = Math.floor(parseInt(speedSelect.value));
running = true;
lastTime = 0;
dropCounter = 0;
score = 0;
level = 1;
updateDisplay();
createBoard();
newPiece();
startBtn.disabled = true;
restartBtn.disabled = false;
update();
}
function stopGame() {
running = false;
startBtn.disabled = false;
restartBtn.disabled = true;
}
window.addEventListener('keydown', e => {
if (!running || gameOver) return;
switch(e.code) {
case 'ArrowLeft':
if(!collide(-1, 0)) pieceX--;
break;
case 'ArrowRight':
if(!collide(1, 0)) pieceX++;
break;
case 'ArrowDown':
dropPiece();
break;
case 'ArrowUp':
case 'Space':
rotatePiece();
break;
}
});
speedSelect.addEventListener('change', () => {
if (running) {
dropInterval = Math.floor(parseInt(speedSelect.value) / level);
}
});
startBtn.addEventListener('click', () => {
resetGame();
startGame();
});
restartBtn.addEventListener('click', () => {
resetGame();
startGame();
});
let touchStartX = 0;
let touchStartY = 0;
const swipeThreshold = 30;
canvas.addEventListener('touchstart', e => {
if(e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
// Новый код для блокировки pull-to-refresh при свайпе вниз на канвасе
let touchStartYForRefresh = 0;
document.addEventListener('touchstart', e => {
if (e.touches.length === 1) {
touchStartYForRefresh = e.touches[0].clientY;
}
}, {passive: true});
document.addEventListener('touchmove', e => {
if (e.touches.length !== 1) return;
const el = e.target;
const currentY = e.touches[0].clientY;
const diffY = currentY - touchStartYForRefresh;
function isCanvasOrInside(element) {
while (element) {
if (element === canvas) return true;
element = element.parentElement;
}
return false;
}
if (diffY > 0 && isCanvasOrInside(el)) {
e.preventDefault();
}
}, {passive: false});
canvas.addEventListener('touchmove', e => {
if(e.touches.length === 1) {
// Не вызываем e.preventDefault(), чтобы позволить скроллить страницу
}
}, { passive: false });
canvas.addEventListener('touchend', e => {
if (!running || gameOver) return;
if(e.changedTouches.length === 1) {
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
if(Math.abs(dx) > Math.abs(dy)) {
if(dx > swipeThreshold) {
if(!collide(1, 0)) pieceX++;
} else if(dx < -swipeThreshold) {
if(!collide(-1, 0)) pieceX--;
}
} else {
if(dy > swipeThreshold) {
fastDrop = true;
dropPiece();
clearTimeout(fastDropTimeout);
fastDropTimeout = setTimeout(() => { fastDrop = false; }, 300);
} else if(dy < -swipeThreshold) {
rotatePiece();
} else {
if(Math.abs(dx) < 10 && Math.abs(dy) < 10) {
rotatePiece();
}
}
}
}
}, { passive: true });
restartBtn.disabled = true;
resetGame();
draw();
})();
</script>
</body>
</html>
Пятнашки с уровнями и правилами
Описание игры
Это классическая логическая головоломка «Пятнашки», где игрок должен собрать все пронумерованные плитки в правильном порядке на квадратном поле с одной пустой клеткой. Размер поля и сложность можно выбрать из трёх вариантов: 3×3, 4×4 (стандартный) и 5×5. Перемещение плиток происходит путём тапов по соседним пустой клетке или с помощью стрелок клавиатуры. Игра ведёт подсчёт ходов и отображает поздравление при успешном решении головоломки. Интуитивно понятный интерфейс и управление адаптированы как для компьютеров, так и для мобильных устройств.
Возможности игры
- Три уровня сложности с размером поля 3×3, 4×4 и 5×5.
- Квадратное игровое поле с квадратными плитками размером от 60×60 до 80×80 пикселей.
- Плитки пронумерованы, а одна клетка остаётся пустой для перемещения.
- Перемещение плиток кликом (или тапом) по соседней пустой клетке.
- Управление с клавиатуры с помощью стрелок, двигающих плитку в пустую сторону.
- Подсчёт количества ходов и динамическое обновление счётчика.
- Проверка достижений задачи: полный правильный порядок плиток.
- Шифрование и проверка, что начальная расстановка всегда решаема (функция
isSolvable). - Кнопка «Перезапустить» для случайной перестановки плиток, начинающей новую игру.
- Информация о количестве ходов и итоговое сообщение о победе.
- Элементы интерфейса снабжены aria-метками, атрибутами для доступности.
- Адаптивный дизайн, корректное отображение и удобное управление на мобильных устройствах и ПК.
- Плавные визуальные эффекты при наведении и взаимодействии с плитками.
- Логика игры полностью реализована на JavaScript без сторонних библиотек.
- Используется CSS Grid, динамически настраиваемый размер сетки через CSS-переменную
--board-size. - Функция
createBoard()генерирует плитки и пустую клетку, назначает атрибуты, табуляцию и обработчики кликов и клавиатурных событий (Enter, пробел) для доступности. - Каждая плитка – это div с классом
.tileи уникальным числовым содержимым, последняя – пустая. - В массиве
boardхранится текущее расположение плиток, 0 обозначает пустую клетку. - Функция
tryMove(index)проверяет, соседствует ли выбранная плитка с пустой, и если да – меняет их местами, увеличивает счётчик ходов, обновляет отображение. - Обновление доски отображается функцией
updateDisplay(), которая синхронизирует DOM с текущим состоянием массиваboard. - Реализована функция
checkWin(), проверяющая упорядоченность плиток по возрастанию, при достижении цели выводит поздравление с количеством ходов. - Функция
isSolved()служит для проверки текущего состояния на решённость. isSolvable()вычисляет количество инверсий для гарантии, что случайная перестановка решаема (с учётом чётности размера поля и положения пустой клетки).shuffleBoard()создаёт случайную перестановку с использованием алгоритма Фишера–Йетса, повторяет перестановку, пока не получится решаемая стартовая позиция и не совпадает с уже собранной.- Обновляет позицию пустой клетки, счётчик ходов и интерфейс.
- Обработчик события
keydownперехватывает стрелки, меняя положение пустой ячейки и плиток. - Обеспечена блокировка обработки клавиш после окончания игры.
- Выпадающий список выбора уровня сложности сразу меняет размер поля, пересоздаёт и перемешивает игровое поле.
- Кнопка «Перезапустить» вызывает новый перемешанный раунд той же сложности.
- Плитки и кнопки снабжены aria-атрибутами, разрешая управление клавиатурой и экранными читалками.
- Визуальные эффекты и переходы делают игру приятной и отзывчивой на любом устройстве.
Подробный разбор кода
1. Инициализация и создание игрового поля
- Получение DOM-элементов: игровое поле (
gameElem), счётчик ходов (infoElem), кнопка перезапуска (restartBtn), вывод результата (resultElem), выбор уровня сложности (levelSelect). - Переменная
sizeхранит текущий размер поля (3, 4 или 5), изначально берётся из выбора. - Функция
createBoard()создаёт сетку изsize × sizeплиток: каждая плитка – div с классомtile, у последней добавляется классemptyи текст пустой. Для каждой плитки навешиваются обработчики клика и клавиатуры (Enter, пробел) для удобства управления. - Сетка формируется с помощью CSS Grid, где количество колонок и рядов задаётся через переменную
--board-size. - Функция
shuffleBoard()генерирует случайную перестановку чисел от 0 доsize² - 1с помощью алгоритма Фишера–Йетса. - Проверяет, что полученная комбинация решаема, вызывая
isSolvable(board), и что она не совпадает с уже собранной позицией (черезisSolved(board)). Если условия не выполняются, перестановка повторяется. - Определяется позиция пустой клетки (
emptyPos) по индексу 0 в массиве. - Обнуляется счётчик ходов и обновляется интерфейс.
updateDisplay()синхронизирует состояние массиваboardс DOM: для каждой плитки устанавливаются классы и тексты, включая aria-метки. Пустая плитка получает классemptyи надпись \u201cПустая клетка\u201d для доступности.- Обновляет текст счётчика ходов.
- Функция
tryMove(index)проверяет, соседствует ли выбранная плитка с пустой клеткой (абсолютная разница по x или y равна 1). - Если соседствует, плитки меняются местами в массиве
board, обновляется позиция пустой клетки (emptyPos), увеличивается счётчик ходов, и вызываетсяupdateDisplay(). - После каждого хода вызывается
checkWin(), которая проверяет, собран ли пазл полностью. checkWin()проверяет, что плитки расположены в порядке возрастания от 1 доsize² - 1. Если да, выводит поздравление с количеством ходов.isSolved()проверяет аналогично, возвращая булево значение для итоговой проверки.isSolvable(array)считает количество инверсий в текущем массиве и учитывает позицию пустой клетки для определения, возможна ли данная перестановка решения согласно правилам классических пятнашек (учёт чётности размера поля).- Обработчик события
keydownотслеживает стрелки вверх, вниз, влево и вправо. - В зависимости от нажатой стрелки вычисляется новая позиция пустой клетки (сдвиг на единицу в противоположную плитку), если она в пределах поля, происходит обмен плиток и обновление состояния.
- После каждого действия тоже вызывается проверка решения.
- Кнопка \u201cПерезапустить\u201d вызывает
shuffleBoard(), начиная новую игру с текущим размером. - Выпадающий список выбора уровня сложности обновляет размер поля, пересоздаёт игровое поле и запускает новую игру.
- Инициализация происходит при загрузке страницы: создаётся поле, перетасовывается, отображается.
- Каждый элемент плитки снабжён tabindex и aria-метками для удобства клавиатурного и экранного доступа.
- Навигация по плиткам возможна клавишами Enter и пробелом.
- Визуальное разделение пустой и заполненных клеток усилено стилями и плавными переходами.
- Подсчёт ходов обновляется в режиме реального времени.
- Отображается итоговое сообщение при решении, блокируя дальнейшие действия до перезапуска.
Полный код игры:
game7.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Пятнашки с уровнями и правилами</title>
<style>
:root {
--board-size: 4;
}
body {
margin: 0; padding: 20px;
background: #121212; color: #eee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
display: flex; flex-direction: column; align-items: center;
user-select: none;
}
h1 {
margin-bottom: 10px;
font-weight: 600;
}
#rules {
background-color: #1a1a1a;
max-width: 600px;
padding: 15px 20px;
border-radius: 12px;
box-shadow: 0 0 15px #000;
font-size: 1rem;
line-height: 1.4;
color: #ccc;
margin-bottom: 20px;
}
#rules h2 {
color: #ffbb33;
margin-top: 0;
font-weight: 600;
text-align: center;
}
#rules ul {
padding-left: 20px;
user-select: none;
}
label {
margin-bottom: 10px;
font-size: 1.1rem;
}
#levelSelect {
margin-bottom: 15px;
font-size: 1.1rem;
padding: 5px 10px;
border-radius: 6px;
}
#game {
display: grid;
gap: 8px;
margin-bottom: 15px;
user-select: none;
grid-template-columns: repeat(var(--board-size), 80px);
grid-template-rows: repeat(var(--board-size), 80px);
}
.tile {
background: #ff4a00;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
font-weight: 700;
cursor: pointer;
box-shadow: 0 0 12px #ff4a00;
user-select: none;
transition: background-color 0.2s;
width: 80px;
height: 80px;
}
.tile.empty {
background: #222;
box-shadow: none;
cursor: default;
}
#info {
margin-bottom: 15px;
font-size: 1.2rem;
}
button {
background-color: #ff4a00;
border: none;
border-radius: 14px;
padding: 10px 30px;
font-size: 1.1rem;
color: white;
font-weight: 600;
cursor: pointer;
box-shadow: 0 0 14px #ff4a00;
user-select: none;
}
button:disabled {
background-color: #555;
box-shadow: none;
cursor: not-allowed;
}
#result {
margin-top: 15px;
font-size: 1.3rem;
min-height: 24px;
text-align: center;
color: #ffbb33;
font-weight: 700;
}
@media (max-width: 480px) {
#game {
grid-template-columns: repeat(var(--board-size), 60px);
grid-template-rows: repeat(var(--board-size), 60px);
}
.tile {
width: 60px;
height: 60px;
font-size: 1.5rem;
border-radius: 8px;
}
button {
padding: 10px 20px;
font-size: 1rem;
}
}
</style>
</head>
<body>
<h1>Пятнашки с уровнями и правилами</h1>
<section id="rules" aria-label="Правила игры в пятнашки">
<h2>Правила игры</h2>
<ul>
<li>Выберите уровень сложности: <strong>3x3</strong>, <strong>4x4</strong> или <strong>5x5</strong>.</li>
<li>Игра состоит из квадратного поля с пронумерованными плитками и одной пустой клеткой.</li>
<li>Цель — собрать все плитки в порядке возрастания (слева направо, сверху вниз) пустая клетка внизу справа.</li>
<li>Для перемещения тапай на плитку рядом с пустой клеткой или используй стрелки клавиатуры.</li>
<li>Счётчик ходов считает каждое перемещение плитки.</li>
<li>Если хочешь начать заново, нажми кнопку «Перезапустить».</li>
<li>Игра адаптирована для смартфонов и компьютеров.</li>
</ul>
</section>
<label for="levelSelect">Выберите уровень сложности:</label>
<select id="levelSelect" aria-label="Выбор уровня пятнашек" name="levelSelect">
<option value="3">Лёгкий (3x3)</option>
<option value="4" selected>Средний (4x4)</option>
<option value="5">Сложный (5x5)</option>
</select>
<div id="game" aria-label="Игровое поле пятнашки" role="application"></div>
<div id="info" aria-live="polite">Ходы: 0</div>
<button id="restart" aria-label="Перезапустить игру">Перезапустить</button>
<div id="result" role="alert" aria-live="assertive"></div>
<script>
const gameElem = document.getElementById('game');
const infoElem = document.getElementById('info');
const restartBtn = document.getElementById('restart');
const resultElem = document.getElementById('result');
const levelSelect = document.getElementById('levelSelect');
let size = parseInt(levelSelect.value);
let board = [];
let emptyPos = {x: size - 1, y: size - 1};
let moves = 0;
function createBoard() {
document.documentElement.style.setProperty('--board-size', size);
gameElem.style.gridTemplateColumns = `repeat(${size}, 80px)`;
gameElem.style.gridTemplateRows = `repeat(${size}, 80px)`;
gameElem.innerHTML = '';
const total = size * size;
for(let i=0; i<total; i++) {
const tile = document.createElement('div');
tile.classList.add('tile');
if(i === total - 1) {
tile.classList.add('empty');
tile.textContent = '';
} else {
tile.textContent = (i + 1).toString();
}
tile.dataset.index = i;
tile.tabIndex = 0;
tile.setAttribute('role', 'button');
tile.setAttribute('aria-label', `Плитка ${tile.textContent || 'пустая'}`);
tile.addEventListener('click', () => tryMove(i));
tile.addEventListener('keydown', e => {
if(e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
tryMove(i);
}
});
gameElem.appendChild(tile);
}
}
function shuffleBoard() {
const total = size * size;
board = [...Array(total).keys()];
do {
for(let i = board.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[board[i], board[j]] = [board[j], board[i]]; // вот исправленный обмен
}
} while(!isSolvable(board) || isSolved(board));
emptyPos = {x: board.indexOf(0) % size, y: Math.floor(board.indexOf(0) / size)};
moves = 0;
updateDisplay();
resultElem.textContent = '';
infoElem.textContent = `Ходы: ${moves}`;
}
function updateDisplay() {
const total = size * size;
for(let i=0; i<total; i++) {
const tile = gameElem.children[i];
const val = board[i];
if(val === 0){
tile.classList.add('empty');
tile.textContent = '';
tile.setAttribute('aria-label', 'Пустая клетка');
} else {
tile.classList.remove('empty');
tile.textContent = val.toString();
tile.setAttribute('aria-label', `Плитка ${val}`);
}
}
infoElem.textContent = `Ходы: ${moves}`;
}
function tryMove(index) {
if(resultElem.textContent.length) return; // игра окончена
const x = index % size;
const y = Math.floor(index / size);
const dx = Math.abs(x - emptyPos.x);
const dy = Math.abs(y - emptyPos.y);
if((dx === 1 && dy === 0) || (dy === 1 && dx === 0)) {
const emptyIndex = emptyPos.y * size + emptyPos.x;
[board[emptyIndex], board[index]] = [board[index], board[emptyIndex]];
emptyPos = {x, y};
moves++;
updateDisplay();
if(checkWin()){
// блокируем клики после победы, можно, например, отключить все обработчики — здесь просто return
// или можно сделать кнопку перезапуска более заметной
}
}
}
function checkWin() {
const total = size * size;
for(let i=0; i<total-1; i++) {
if(board[i] !== i+1) return false;
}
resultElem.textContent = `Поздравляю! Ты собрал пятнашки ${size}x${size} за ${moves} ходов!`;
return true;
}
function isSolved(arr) {
for(let i=0; i<arr.length-1; i++) {
if(arr[i] !== i+1) return false;
}
return true;
}
function isSolvable(array) {
let invCount = 0;
const total = size*size;
for(let i=0; i<total-1; i++) {
for(let j=i+1; j<total; j++) {
if(array[i] && array[j] && array[i] > array[j]) invCount++;
}
}
const emptyRow = Math.floor(array.indexOf(0) / size);
if(size % 2 === 1) return invCount % 2 === 0;
else {
const blankRowFromBottom = size - 1 - emptyRow;
if(blankRowFromBottom % 2 === 0) return invCount % 2 === 1;
else return invCount % 2 === 0;
}
}
window.addEventListener('keydown', (e) => {
if(resultElem.textContent.length) return;
let dx = 0, dy = 0;
switch(e.key){
case 'ArrowUp': dy = 1; break;
case 'ArrowDown': dy = -1; break;
case 'ArrowLeft': dx = 1; break;
case 'ArrowRight': dx = -1; break;
default: return;
}
e.preventDefault();
const newX = emptyPos.x + dx;
const newY = emptyPos.y + dy;
if(newX >= 0 && newX < size && newY >= 0 && newY < size){
const newIndex = newY * size + newX;
const emptyIndex = emptyPos.y * size + emptyPos.x;
[board[emptyIndex], board[newIndex]] = [board[newIndex], board[emptyIndex]];
emptyPos = {x: newX, y: newY};
moves++;
updateDisplay();
checkWin();
}
});
restartBtn.addEventListener('click', () => {
shuffleBoard();
});
levelSelect.addEventListener('change', () => {
size = parseInt(levelSelect.value);
emptyPos = {x: size - 1, y: size - 1};
createBoard();
shuffleBoard();
});
createBoard();
shuffleBoard();
</script>
</body>
</html>
Главная страница «Сборник игр»
Описание страницы
Это удобная и стильная стартовая страница, служащая центральным каталогом для доступа к различным веб-играм из сборника. Она адаптирована под разные устройства и содержит краткий навигационный список всех доступных игр с лёгким переключением между тёмной и светлой темами интерфейса. Интерфейс интуитивно понятен и современен, благодаря аккуратному дизайну с комфортным размером элементов и плавным эффектам при наведении.
Возможности страницы
- Адаптивный дизайн: страница корректно отображается на экранах с шириной до 480 пикселей и более, с плавной сменой размеров шрифтов и кнопок.
- Переключение темы: кнопка «Светлая тема» / «Тёмная тема» позволяет менять цветовую схему сайта, улучшая комфорт при использовании в разное время суток.
- Навигация по играм:
- Список из семи игр с наименованиями и кликабельными ссылками.
- Стильные ссылки с цветовой подсветкой и эффектом «подъёма» при наведении курсора.
- Каждая игра представлена в виде крупного блока с отступами и скруглёнными углами.
- Использование системных шрифтов: для быстрой загрузки и комфортного чтения текста.
- Плавные переходы: для цветов, теней и трансформаций, обеспечивающие современный и приятный интерфейс.
- Доступность:
- Кнопка смены темы снабжена aria-меткой для удобства пользователей с вспомогательными технологиями.
- Удобная клавиатурная доступность благодаря правильным элементам и фокусам.
- Чистый и лёгкий код:
- Использован минималистичный CSS без сторонних библиотек.
- Логика темы на JavaScript – небольшая и эффективная.
- Структурированность:
- Хедер с заголовком и кнопкой.
- Список игр в виде маркированного списка.
- Поддержка плавной смены содержимого кнопки в зависимости от состояния темы.
- Подходит как для ПК, так и для мобильных устройств:
- Оптимизация размеров и пропорций элементов.
- Отсутствие горизонтального скроллинга.
<header>содержит заголовок<h1>и кнопку переключения темы с классом.toggle-theme.- Игры перечислены в
<ul>списке, каждая игра – это<li>с ссылкой<a>, ведущей на соответствующую страницу. - Каждый элемент ссылки стилизован в виде блока с фоном, паддингами и скруглением.
- Основной фон страницы и цвет текста задаются для классического тёмного режима.
- Класс
.lightменяет цвета фона и текста, а также стили ссылок для светлой темы. - Используются эффекты
transitionдля плавности изменения цветов и трансформаций. - Для ссылок (игр) применены тени, цветовые переходы и эффект подъёма при наведении.
- Медиа-запросы для устройств с шириной до 480px уменьшают размеры шрифтов и паддингов.
- Находит кнопку переключения темы и тег
<body>. - Функция
updateButtonTextменяет текст кнопки в зависимости от текущей темы. - Обработчик клика на кнопку переключает класс
lightу<body>, вызывая смену темы. - При загрузке страницы сразу вызывается
updateButtonTextдля корректного начального состояния кнопки.
Подробный разбор кода
HTML
- Страница структурирована просто и логично.
- В
<header>расположен заголовок<h1>с названием «Сборник игр» и кнопка для переключения темы с классом.toggle-theme. - Кнопка снабжена aria-атрибутом
aria-labelдля доступности, поясняя её функцию. - Игры представлены списком
<ul>, где каждая игра – это пункт<li>с ссылкой<a>на отдельную страницу игры. - Весь код обёрнут в стандартные теги
<html lang="ru">и<head>с нужными мета-тегами (charset,viewport,title). - Базовые стили устанавливают тёмный фон, светлый цвет текста и системный шрифт для быстрой загрузки и удобства чтения.
- Используется flexbox для центрирования и вертикального распределения содержимого.
- Кнопка переключения темы стилизована с прозрачным фоном, рамкой и плавным эффектом при наведении (смена цвета и фона).
- Ссылки-игры оформлены как блоки с фоном, скруглением и внутренними отступами. При наведении плавно меняют фон, цвет текста, добавляют тень и лёгкое смещение вверх.
- При включении светлой темы через класс
.lightменяется основной фон, цвет текста и стили ссылок и кнопки, обеспечивая контраст и читаемость. - Для экранов шириной меньше 480px применены медиа-запросы, уменьшающие размеры шрифтов и внутренних отступов кнопок и ссылок.
- При загрузке страницы выбирается кнопка переключения темы и элемент
<body>. - Функция
updateButtonText()обновляет текст кнопки в зависимости от текущей темы: если у<body>есть класс.light, текст кнопки меняется на «Тёмная тема», иначе – на «Светлая тема». - Обработчик клика по кнопке переключает класс
.lightу<body>и вызывает функцию обновления текста кнопки. - Сценарий аккуратный, минималистичный и обеспечивает удобное переключение темы без перезагрузки страницы.
Полный код главной страницы:
index.htmll:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Сборник игр</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* Используем системный шрифт без облачных подключений */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
background-color: #121212;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 40px 20px;
transition: background-color 0.3s, color 0.3s;
}
.light {
background-color: #f5f7fa;
color: #222;
}
header {
width: 100%;
max-width: 480px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
h1 {
font-weight: 600;
font-size: 2.5rem;
margin: 0;
}
button.toggle-theme {
background: none;
border: 2px solid currentColor;
border-radius: 24px;
padding: 6px 16px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
color: inherit;
transition: background-color 0.3s, color 0.3s;
}
button.toggle-theme:hover {
background-color: currentColor;
color: #121212;
}
ul {
list-style: none;
padding: 0;
width: 100%;
max-width: 480px;
}
li {
margin-bottom: 18px;
}
a {
display: block;
background-color: #1f1f1f;
color: #90caf9;
padding: 16px 24px;
border-radius: 12px;
text-decoration: none;
font-weight: 600;
font-size: 1.15rem;
box-shadow: 0 3px 8px rgba(0,0,0,0.8);
transition: background-color 0.3s, color 0.3s, box-shadow 0.3s, transform 0.3s;
user-select: none;
}
.light a {
background-color: #e3e9f6;
color: #1a73e8;
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
a:hover {
background-color: #90caf9;
color: #121212;
box-shadow: 0 8px 20px rgba(144,202,249,0.7);
transform: translateY(-3px);
}
.light a:hover {
background-color: #1a73e8;
color: #f5f7fa;
box-shadow: 0 8px 20px rgba(26,115,232,0.5);
}
@media (max-width: 480px) {
h1 {
font-size: 2rem;
}
a {
font-size: 1.05rem;
padding: 14px 20px;
}
}
</style>
</head>
<body>
<header>
<h1>Сборник игр</h1>
<button class="toggle-theme" aria-label="Переключить тему">Светлая тема</button>
</header>
<ul>
<li><a href="game1.html">Кликер на снаряды</a></li>
<li><a href="game2.html">Платформер</a></li>
<li><a href="game3.html">Музыкальный реактор</a></li>
<li><a href="game4.html">Мини-гольф с физикой</a></li>
<li><a href="game5.html">Три в ряд</a></li>
<li><a href="game6.html">Тетрис</a></li>
<li><a href="game7.html">Пятнашки</a></li>
</ul>
<script>
const button = document.querySelector('.toggle-theme');
const body = document.body;
function updateButtonText() {
button.textContent = body.classList.contains('light') ? 'Тёмная тема' : 'Светлая тема';
}
button.addEventListener('click', () => {
body.classList.toggle('light');
updateButtonText();
});
updateButtonText();
</script>
</body>
</html>
Платформер с уровнями сложности
Описание игры
Игрок управляет рыжим персонажем на основе прямоугольника, который может ходить влево и вправо и прыгать по платформам, чтобы добраться до зелёного квадрата – цели уровня. В игре несколько уровней с возрастающей сложностью, на которых присутствуют враги и ограничения по времени. Попадание на врага или истечение времени приводит к перезапуску уровня. Есть возможность выбрать уровень сложности, который влияет на количество и поведение врагов, а также на продолжительность таймера.
В игре предусмотрена поддержка управления с клавиатуры (стрелки, пробел, WASD) и сенсорных кнопок на экране (для мобильных устройств). На экране отображается счёт, номер текущего уровня, таймер, а также выбор сложности и кнопка запуска.
Возможности игры
- HTML-элементы: canvas для игрового поля, кнопки сенсорного управления (
btnLeft,btnRight,btnJump), выпадающий список сложности, кнопка старта, панель информации с количеством очков, уровнем и таймером. - Глобальные переменные:
score– очки игрока;currentLevel– текущий номер уровня;gameStarted– флаг запущенной игры;timeLeft– оставшееся время на уровне;levelsData– объект с данными уровней по сложности;player– объект с параметрами персонажа (координаты, размеры, скорость, падение и состояние на земле);platforms,goal,enemies– данные текущего уровня;keys– объект для отслеживания нажатий движений;- Настройка гравитации (
gravity= 0.5). - Загружаются платформы, цели и враги из
levelsDataвыбранной сложности и указанного уровня. - Инициализируются начальные координаты и скорости игрока.
- Сбрасывается таймер и запускается время на уровень.
- Обновляется отображение: номер уровня и таймер.
- Флаг
gameStartedстановитсяtrue. - Проверяется состояние управления: если нажата левая или правая кнопка, меняется горизонтальная скорость.
- Позиция игрока обновляется с учётом скорости.
- Гравитация влияет на вертикальную скорость и положение.
- Происходит проверка пересечения игрока с платформами – если игрок стоит на платформе, сбрасывается вертикальная скорость и включается флаг
grounded. - Обрабатывается ограничение выхода игрока за границы канваса.
- Враги двигаются в пределах заданного диапазона скорости и направления.
- Проверяется столкновение с врагами – если есть, уровень перезапускается.
- Проверяется достижение цели – если персонаж касается зелёного квадрата, счёт увеличивается, уровень меняется (или игра заканчивается, если конец).
- Таймер уменьшается каждую секунду, если время вышло – уровень перезапускается.
- Очищается канвас.
- Отрисовываются платформы серым цветом.
- Цель отрисовывается зелёным.
- Враги красным.
- Игрок рыжим прямоугольником.
- События
keydownиkeyupна документе: - Влево (ArrowLeft, A) –
keys.left = true; - Вправо (ArrowRight, D) –
keys.right = true; - Прыжок (ArrowUp, W, Space) – если игрок на земле, вертикальная скорость устанавливается отрицательной (вверх).
- Для кнопок слева, справа, прыжка:
- Обработчики
touchstart,touchend,mousedown,mouseup,mouseleave; - При нажатии задаются флаги движения или прыжка;
- Кнопки получают класс
.pressedдля визуальной индикации; - При отпускании флаги сбрасываются.
- Можно выбрать сложность игры из трёх вариантов (
easy,medium,hard) с разным количеством уровней и параметрами врагов и времени. - При старте игра блокирует выбор сложности, показывает панель со счётом и таймером.
- Игра показывает уведомления при столкновении с врагом и при прохождении всех уровней.
- Есть адаптивное изменение размеров canvas на изменение ширины окна.
- Полноценная 2D физика с гравитацией и коллизиями.
- Управление клавиатурой и сенсорными кнопками.
- Многоуровневая схема с изменением сложности.
- Таймер и система очков с реакцией на события.
- Простая, но понятная визуализация.
- Паузы и сохранение прогресса.
- Улучшенную физику прыжков.
- Анимации персонажа и врагов.
- Звуковое сопровождение.
Подробный разбор кода
1. Глобальные переменные и элементы DOM
- Canvas и контекст:
- Интерфейсные элементы:
- Игровые данные:
- Игрок:
- Движение:
- Гравитация: постоянное ускорение вниз,
gravity = 0.5. - Движение игрока по горизонтали:
- Обновление позиции:
- Гравитация и столкновение с платформами:
- Границы канваса:
- Враги двигаются:
- Проверка столкновений с врагами:
- Проверка достижения цели:
- Обработка таймера:
- Очищается холст (
clearRect). - Рисуются платформы (серым).
- Рисуется цель (зелёным).
- Рисуются враги (красным).
- Рисуется игрок (рыжим прямоугольником).
- На
documentповешены слушатели: keydown– при соответствующем коде клавиши (ArrowLeft,KeyAи др.) включаются флаги движенияkeys.left,keys.right. Если нажата кнопка прыжка (стрелка вверх, пробел, W) и игрок на земле, задаётся вертикальная скорость прыжкаdy = -jumpStrength.keyup– сбрасывают флаги движения.- Фильтрация по
e.repeat– чтобы не обрабатывать зажатие клавиши многократно. - Для каждой кнопки (
btnLeft,btnRight,btnJump) повешены слушатели событий: touchstartиmousedownактивируют соответствующую команду (движение влево/вправо или прыжок).touchend,mouseup,mouseleave– отключают движение, снимают стили кнопки.- Кнопки визуально подсвечиваются классом
pressedпри удержании. - Для прыжка прыжок срабатывает единожды при нажатии, если игрок на земле.
- Вызывается
update– обновление игрового состояния. - Вызывается
draw– прорисовка. - Если игра активна, вызывается
requestAnimationFrame(gameLoop)для следующего кадра. - Копирует данные платформ, цели и врагов из объекта уровней выбранной сложности.
- Возвращает игрока на начальную позицию.
- Сбрасывает скорости, флаги и таймер уровня.
- Обновляет отображение текущего уровня и таймера.
- Устанавливает флаг
gameStarted = true. - При нажатии кнопки
#startBtn: - Скрывается кнопка запуска.
- Блокируется выбор сложности.
- Показывается холст и панель информации.
- Отображаются сенсорные кнопки.
- Сбрасываются очки и уровень.
- Загружается первый уровень.
- Запускается цикл игры.
- Игра собрана по классической схеме 2D платформера с обходом врагов и прыжками.
- Управление как с клавиатуры, так и с сенсорных кнопок.
- Для удобства пользователя есть визуальный контроль остатков времени, очков и уровня.
- Используется объектная структура уровней для удобства настройки.
- Реализованы базовые физика гравитации и коллизии.
Полный код игры
game2.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Платформер с уровнями сложности</title>
<style>
body {
margin: 0; padding: 0;
background: #222;
font-family: Arial, sans-serif;
color: #eee;
text-align: center;
user-select: none;
}
h1 {
margin: 20px 0 10px;
font-size: 1.5rem;
}
#rules {
max-width: 600px;
margin: 5px auto 10px;
padding: 10px 15px;
background: #333;
border-radius: 12px;
box-shadow: 0 0 15px #000;
font-size: 14px;
line-height: 1.4;
}
#gameControls {
margin: 10px auto 15px;
max-width: 700px;
}
select {
padding: 8px 12px;
font-size: 16px;
border-radius: 8px;
border: none;
background: #444;
color: #eee;
cursor: pointer;
user-select: none;
}
#infoPanel {
margin-top: 6px;
font-size: 18px;
display: none;
justify-content: center;
gap: 20px;
}
#score, #levelDisplay, #timer {
user-select: none;
-webkit-user-select: none;
}
canvas {
background: #333;
display: none;
margin: 15px auto 5px;
border: 3px solid #555;
border-radius: 12px;
max-width: 95vw;
height: auto;
}
#mobileControls {
display: none;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
}
.btnControl {
width: 75px;
height: 75px;
background: #555;
border-radius: 50%;
box-shadow: 0 0 15px #999;
font-size: 30px;
font-weight: bold;
color: #eee;
line-height: 75px;
text-align: center;
user-select: none;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
}
.btnControl.pressed {
background: #ff7f50;
box-shadow: 0 0 25px #ff7f50;
color: #fff;
}
#startBtn {
margin: 20px auto 30px;
display: inline-block;
padding: 14px 40px;
font-size: 24px;
font-weight: 700;
border-radius: 14px;
background: #ff4a00;
color: #fff;
cursor: pointer;
box-shadow: 0 0 20px #ff4a00;
user-select: none;
border: none;
transition: background 0.3s, box-shadow 0.3s;
}
#startBtn:hover {
background: #ff6633;
box-shadow: 0 0 30px #ff6633;
}
</style>
</head>
<body>
<h1>Платформер с уровнями сложности</h1>
<div id="rules" aria-label="Правила игры">
<strong>Правила:</strong>
<ul style="text-align:left; padding-left: 20px; margin: 5px 0;">
<li>Управление стрелками влево-вправо и пробел для прыжка (или сенсорными кнопками).</li>
<li>Цель - дойти до зелёного квадрата (цель).</li>
<li>Враги ходят по платформам. Столкновение с врагом - перезапуск уровня.</li>
<li>Каждый уровень - таймер, по истечении которого нужно начинать заново.</li>
<li>Выбирай сложность; чем выше, тем сложнее уровни и меньше времени.</li>
<li>После прохождения всех уровней поздравление и сброс счёта.</li>
</ul>
</div>
<div id="gameControls">
Сложность:
<select id="difficultySelect" aria-label="Выбор сложности">
<option value="easy">Лёгкий</option>
<option value="medium" selected>Средний</option>
<option value="hard">Сложный</option>
</select>
</div>
<div id="infoPanel">
<div id="levelDisplay">Уровень: 1</div>
<div id="score">Очки: 0</div>
<div id="timer">Время: 0</div>
</div>
<canvas id="gameCanvas" width="700" height="400" aria-label="Платформер"></canvas>
<div id="mobileControls" aria-label="Сенсорные кнопки управления">
<div id="btnLeft" class="btnControl" role="button" tabindex="0" aria-pressed="false">◄</div>
<div id="btnJump" class="btnControl" role="button" tabindex="0" aria-pressed="false">▲</div>
<div id="btnRight" class="btnControl" role="button" tabindex="0" aria-pressed="false">►</div>
</div>
<button id="startBtn" type="button" aria-label="Начать игру">Начать игру</button>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElem = document.getElementById('score');
const levelDisplay = document.getElementById('levelDisplay');
const timerElem = document.getElementById('timer');
const difficultySelect = document.getElementById('difficultySelect');
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnJump = document.getElementById('btnJump');
const startBtn = document.getElementById('startBtn');
const infoPanel = document.getElementById('infoPanel');
const mobileControls = document.getElementById('mobileControls');
const gravity = 0.5;
let score = 0;
let currentLevel = 0;
let gameStarted = false;
let timeLeft = 0;
const levelsData = {
easy: [
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:150, y:280, width:120, height:15},
{x:350, y:230, width:120, height:15},
{x:550, y:180, width:120, height:15},
],
goal: {x: 620, y:130, width:30, height:30, color:'#0f0'},
enemies: [ {x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}} ],
time: 90
},
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:100, y:300, width:150, height:15},
{x:350, y:250, width:150, height:15},
{x:550, y:200, width:120, height:15},
],
goal: {x:620, y:170, width:30, height:30, color:'#0f0'},
enemies: [ {x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:450}} ],
time: 85
}
],
medium: [
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:150, y:280, width:120, height:15},
{x:350, y:230, width:120, height:15},
{x:550, y:180, width:120, height:15},
],
goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
enemies: [
{x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}},
{x:400, y:195, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:400,max:650}}
],
time: 80
},
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:100, y:300, width:150, height:15},
{x:350, y:250, width:150, height:15},
{x:550, y:200, width:150, height:15},
],
goal: {x:620, y:150, width:30, height:30, color:'#0f0'},
enemies: [
{x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:400}},
{x:550, y:165, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:550,max:680}}
],
time: 75
}
],
hard: [
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:130, y:300, width:140, height:15},
{x:350, y:250, width:140, height:15},
{x:550, y:190, width:140, height:15},
],
goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
enemies: [
{x:10, y:315, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:10,max:650}},
{x:420, y:215, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:420,max:650}},
{x:560, y:155, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:560,max:680}}
],
time: 60
},
{
platforms: [
{x:0, y:350, width:700, height:50},
{x:120, y:320, width:80, height:15},
{x:280, y:270, width:100, height:15},
{x:430, y:220, width:110, height:15},
{x:600, y:170, width:90, height:15}
],
goal: {x:650, y:120, width:30, height:30, color:'#0f0'},
enemies: [
{x:120, y:305, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:120,max:200}},
{x:280, y:255, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:280,max:380}},
{x:430, y:205, width:40, height:35, color:'#f00', speed:6, direction:1, range:{min:430,max:540}},
{x:600, y:155, width:40, height:35, color:'#f00', speed:7, direction:1, range:{min:600,max:690}}
],
time: 50
}
]
};
const player = {
x: 50,
y: 0,
width: 30,
height: 50,
color: '#ff9933',
dy: 0,
dx: 0,
speed: 4,
jumpStrength: 12,
grounded: false,
};
let platforms = [];
let goal = null;
let enemies = [];
const keys = { left: false, right: false };
function rectsCollide(r1, r2) {
return !(r1.x > r2.x + r2.width || r1.x + r1.width < r2.x ||
r1.y > r2.y + r2.height || r1.y + r1.height < r2.y);
}
function update() {
if(!gameStarted) return;
player.dx = 0;
if(keys.left) player.dx = -player.speed;
if(keys.right) player.dx = player.speed;
player.x += player.dx;
player.dy += gravity;
player.y += player.dy;
player.grounded = false;
for(let platform of platforms){
if(
player.x < platform.x + platform.width &&
player.x + player.width > platform.x &&
player.y + player.height >= platform.y &&
player.y + player.height <= platform.y + platform.height &&
player.dy >= 0
) {
player.y = platform.y - player.height;
player.dy = 0;
player.grounded = true;
}
}
if(player.x < 0) player.x = 0;
if(player.x + player.width > canvas.width) player.x = canvas.width - player.width;
if(player.y + player.height > canvas.height){
player.y = canvas.height - player.height;
player.dy = 0;
player.grounded = true;
}
for(let enemy of enemies){
enemy.x += enemy.speed * enemy.direction;
if(enemy.x > enemy.range.max || enemy.x < enemy.range.min){
enemy.direction *= -1;
}
}
for(let enemy of enemies){
if(rectsCollide(player, enemy)){
alert('Ты столкнулась с врагом! Уровень заново.');
loadLevel(currentLevel);
return;
}
}
if(rectsCollide(player, goal)){
score++;
scoreElem.textContent = 'Очки: ' + score;
currentLevel++;
if(currentLevel >= levelsData[difficultySelect.value].length){
alert('Отлично! Ты прошла все уровни со счётом: ' + score);
currentLevel = 0;
score = 0;
scoreElem.textContent = 'Очки: 0';
}
loadLevel(currentLevel);
}
if(timeLeft > 0){
timeLeft -= 1/60;
timerElem.textContent = 'Время: ' + Math.ceil(timeLeft);
if(timeLeft <= 0){
alert('Время вышло! Уровень заново.');
loadLevel(currentLevel);
}
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#999';
for(let platform of platforms){
ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
}
ctx.fillStyle = goal.color;
ctx.fillRect(goal.x, goal.y, goal.width, goal.height);
for(let enemy of enemies){
ctx.fillStyle = enemy.color;
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
}
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);
}
function jump() {
if(player.grounded) {
player.dy = -player.jumpStrength;
}
}
function onKeyDown(e) {
if(!gameStarted) return;
if(e.repeat) return;
if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if((e.code === 'ArrowUp' || e.code === 'Space' || e.code === 'KeyW')){
jump();
}
}
function onKeyUp(e) {
if(!gameStarted) return;
if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
}
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
function bindControl(btn, key) {
btn.addEventListener('touchstart', e => {
e.preventDefault();
if(!gameStarted) return;
if(key === 'jump') jump();
else keys[key] = true;
btn.classList.add('pressed');
}, {passive: false});
btn.addEventListener('touchend', e => {
e.preventDefault();
if(!gameStarted) return;
if(key !== 'jump') keys[key] = false;
btn.classList.remove('pressed');
}, {passive: false});
btn.addEventListener('mousedown', e => {
e.preventDefault();
if(!gameStarted) return;
if(key === 'jump') jump();
else keys[key] = true;
btn.classList.add('pressed');
});
btn.addEventListener('mouseup', e => {
e.preventDefault();
if(!gameStarted) return;
if(key !== 'jump') keys[key] = false;
btn.classList.remove('pressed');
});
btn.addEventListener('mouseleave', e => {
e.preventDefault();
if(!gameStarted) return;
if(key !== 'jump') keys[key] = false;
btn.classList.remove('pressed');
});
}
bindControl(btnLeft, 'left');
bindControl(btnRight, 'right');
bindControl(btnJump, 'jump');
function gameLoop() {
update();
draw();
if(gameStarted) requestAnimationFrame(gameLoop);
}
function loadLevel(index) {
const lvl = levelsData[difficultySelect.value][index];
platforms = [...lvl.platforms];
goal = {...lvl.goal};
enemies = lvl.enemies ? lvl.enemies.map(e => ({...e})) : [];
player.x = 50;
player.y = 0;
player.dy = 0;
player.dx = 0;
player.grounded = false;
timeLeft = lvl.time;
levelDisplay.textContent = 'Уровень: ' + (index+1) + ` (Сложность: ${difficultySelect.options[difficultySelect.selectedIndex].text})`;
timerElem.textContent = 'Время: ' + timeLeft;
gameStarted = true;
}
startBtn.addEventListener('click', () => {
startBtn.style.display = 'none';
difficultySelect.disabled = true;
canvas.style.display = 'block';
infoPanel.style.display = 'flex';
mobileControls.style.display = 'flex';
score = 0;
scoreElem.textContent = 'Очки: 0';
currentLevel = 0;
loadLevel(currentLevel);
gameLoop();
});
difficultySelect.addEventListener('change', () => {});
function resizeCanvas() {
const ratio = canvas.width / canvas.height;
let width = window.innerWidth * 0.95;
if(width > 700) width = 700;
canvas.style.width = width + 'px';
canvas.style.height = (width / ratio) + 'px';
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
</script>
</body>
</html>